Таймер в Тильде: одна строка в ядре и полсекунды из вашей кнопки
Блок-таймер у Тильды есть: его класс `.t-timer` прописан в ядре анимации наравне с `.t-title` и `.t-btn`. Но вся эта прописка — одна строка, и она не про отсчёт, а про то, сколько ждать перед показом. Зато у неё есть цена: таймер отодвигает кнопку на полсекунды, а на блоке с тремя кнопками порядок появления ломается совсем. Плюс разберём единственный настоящий отсчёт, который Тильда написала сама, — он считает не датой, а цифрой из разметки.
Про таймер в Тильде спрашивают двумя способами: «есть ли штатный» и «как
сделать свой». Ответ на оба выясняется в одном месте — в ядре анимации, где
Тильда один раз упоминает класс .t-timer. Эта строка отвечает и на «есть ли»,
и на «что будет, если поставить». Разберём её, потом посмотрим на единственный
отсчёт, который Тильда написала сама, и на то, почему в Zero Block таймера нет
и не может быть.
Короткий ответ
Блок-таймер у Тильды есть — .t-timer прописан в ядре анимации наравне с
заголовком и кнопкой, — но его кода в разобранном рантайме нет: он в постраничном
бандле. Единственное, что ядро с таймером делает, — отодвигает появление на
полсекунды, и всё, что стоит после него, съезжает. В Zero Block типа «таймер»
нет: развилка закрыта десятью значениями.
Одна строка, которая доказывает, что блок есть
Вот вся прописка таймера в ядре анимации tilda-animation-2.0.min.js. Функция
t_animate__assignGroupDelay выписана целиком, вместе с объявлениями выборок —
без них порядок не читается:
function t_animate__assignGroupDelay(t, e) {
var a = 0;
if (t.querySelectorAll("[data-animate-order]").length)
t_animate__assignOrderedElemsDelay(t, e);
else {
var n = t.querySelectorAll(".t-img.t-animate"),
i = t.querySelectorAll(".t-uptitle.t-animate"),
r = t.querySelectorAll(".t-title.t-animate");
r = Array.prototype.filter.call(r, function (t) {
return !t.classList.contains("t-section__title") });
var o = t.querySelectorAll(".t-descr.t-animate");
o = Array.prototype.filter.call(o, function (t) {
return !t.classList.contains("t-section__descr") });
var s = t.querySelectorAll(".t-btn.t-animate, .t-btnwrapper.t-animate");
s = Array.prototype.filter.call(s, function (t) {
return !t.closest(".t-section__bottomwrapper") });
var l = t.querySelectorAll(".t-timer.t-animate"),
m = t.querySelectorAll("form.t-animate");
n.length && (a = .5),
r.length && …(r → transitionDelay = a), r.length && (a += .3),
o.length && …(o → transitionDelay = a), o.length && (a += .3),
i.length && …(i → transitionDelay = a), i.length && (a += .3),
(i.length || r.length || o.length) && (a += .2),
l.length && …(l → transitionDelay = a), l.length && (a += .5),
s.length && (s[0].style.transitionDelay = a + "s"),
2 === s.length && (a += .4),
2 === s.length && (s[1].style.transitionDelay = a + "s"),
0 !== m.length && …(m → transitionDelay = a)
}
}
Что тут важно. .t-timer стоит в одном ряду с .t-img, .t-uptitle,
.t-title, .t-descr, .t-btn и form — то есть ядро Тильды знает таймер как
полноправный элемент содержимого блока, ровно как заголовок. Так не пишут про
чужой класс. Это и есть доказательство, что штатный блок-таймер у Тильды
существует.
И одновременно это всё, что ядро с таймером делает: присваивает ему
style.transitionDelay. Ни даты, ни отсчёта, ни поведения при нуле, ни циклов —
в этом файле про них нет ничего. t-timer встречается в нём ровно один раз при
контрольных 153 вхождениях t-animate.
Цена таймера в очереди появления: полсекунды
Очередь читается по приращениям a. Порядок такой: заголовок → описание →
надзаголовок → таймер → первая кнопка → вторая кнопка → форма. Стоимость
каждого шага: 0.3 с у заголовка, описания и надзаголовка, потом общая надбавка
0.2 с, если был хоть какой-то текст, 0.5 с у таймера, 0.4 с между двумя
кнопками.
Полсекунды — самый крупный единичный шаг в цепочке. Столько же стоит только наличие картинки.
Посчитаем блок «картинка + заголовок + описание + таймер + две кнопки»:
| элемент | задержка |
|---|---|
| картинка | 0 |
| заголовок | 0.5 с |
| описание | 0.8 с |
| таймер | 1.3 с |
| первая кнопка | 1.8 с |
| вторая кнопка | 2.2 с |
Без таймера первая кнопка приехала бы на 1.3 с, вторая на 1.7 с. То есть на промо-блоке, где таймер и стоит, кнопка — та самая, на которую и рассчитывают жать, — приезжает последней и с задержкой больше двух секунд. Причём это только задержка: само проявление идёт сверху, своей длительностью.
Картинке задержка не назначается вообще. Обратите внимание:
n.length && (a = .5) — наличие картинки только сдвигает старт для всех
остальных, но сама выборка n ни разу не перебирается. Картинка появляется
без задержки, первой, всегда.
Надзаголовок появляется после описания. Он стоит выше заголовка визуально, а
задержку получает третьим — после заголовка и описания. Это не опечатка в коде,
это порядок вычислений: r, потом o, потом i.
Три кнопки ломают порядок
Здесь в коде настоящая ловушка, и её видно только если посмотреть на условия:
s.length && (s[0].style.transitionDelay = a + "s"),
2 === s.length && (a += .4),
2 === s.length && (s[1].style.transitionDelay = a + "s"),
Задержку получает s[0] — всегда. И s[1] — только когда кнопок ровно две.
Если кнопок три и больше, вторая и третья не получают задержки вообще: у них
остаётся то, что дал CSS, то есть ноль. Итог: на блоке с тремя кнопками первая
кнопка выезжает последней, через 1.8 секунды, а вторая и третья — сразу, вместе с
картинкой. Порядок вывернут наизнанку.
Обходится это переключением на ручной порядок: если внутри блока есть хоть один
элемент с data-animate-order, вся функция уходит в другую ветку —
t_animate__assignOrderedElemsDelay, — и цепочка по классам не применяется
совсем.
Единственный настоящий отсчёт, который Тильда написала сама
Он живёт в корзине T706 и считает не до даты, а до нуля: это плашка «товар
удалён, вернуть». Вот её setInterval, из tilda-cart-1.1.min.js:
var m = setInterval(function () {
var t = l.querySelector(".t706__product-deleted__timer__counter__number"),
e = t.innerText, o;
window.tcart_fullscreen
? document.body.classList.contains("t706__body_cartsidebarshowed")
? o = !document.querySelector(".t706__sidebar")
.classList.contains("t706__sidebar_showed")
: document.body.classList.contains("t706__body_cartpageshowed") &&
(o = !document.querySelector(".t706__cartpage")
.classList.contains("t706__cartpage_showed"))
: o = !document.querySelector(".t706__cartwin")
.classList.contains("t706__cartwin_showed"),
o && clearInterval(m),
e > 1
? t.innerText = parseInt(e, 10) - 1
: (clearInterval(m), tcart_fadeOut(l, 200, function () { … }))
}, 1e3);
Три свойства, которые стоит перенять и одно — которое стоит не перенимать.
Считает цифрой из разметки, а не датой. e = t.innerText, потом
t.innerText = parseInt(e, 10) - 1. Число живёт в DOM, интервал его уменьшает.
Такой счётчик не знает, сколько времени прошло на самом деле: если браузер
придушил интервал — фоновая вкладка, слабый телефон, тяжёлый скрипт рядом —
секунды теряются и не догоняются. Ровно так и рождается «у всех показывает
разное». Свой таймер надо считать от Date.now(), а не от предыдущего значения.
Скрыт — значит стоп. Каждый тик проверяет, на месте ли класс _showed у
контейнера корзины, и если контейнер спрятан, интервал убивает себя. Это
соглашение самой Тильды, и оно разумное: считать в невидимом блоке незачем.
Но проверка стоит без выхода. o && clearInterval(m) — и дальше, без
return, всё равно исполняется e > 1 ? …. То есть на том самом тике, когда
контейнер уже скрыт, цифра уменьшается ещё раз. Мелочь, но именно из таких
мелочей потом получается «показало на секунду меньше».
А кольцо живёт своей жизнью. Индикатор вокруг цифры — CSS-анимация из
tilda-cart-1.0.min.css:
.t706__product-deleted__timer__counter__circle circle {
stroke-dasharray: 100; stroke-dashoffset: 0; stroke-width: 2px;
animation: t706_countdown 4s linear infinite forwards
}
@keyframes t706_countdown { 0% { stroke-dashoffset: 35 } to { stroke-dashoffset: 102 } }
Период кольца — 4 с, и цифра тоже стартует с 4 и через четыре тика исчезает,
так что при нормальном ходе они совпадают. А вот infinite здесь лишний ровно
до того момента, когда корзину закрывают: интервал умирает, элемент остаётся,
и кольцо продолжает крутиться по кругу над замершей цифрой. Тот же расчёт в
своей вёрстке даст тот же расход: одна половина отсчёта на JS, другая на CSS —
и они разъезжаются при первой же паузе.
В Zero Block типа «таймер» нет, и это следствие, а не придирка
Все элементы Zero Block создаются в одной развилке, t396_artboard_build:
switch (t.getAttribute("data-elem-type")) {
case "text": t396_addText(r, t); break;
case "image": t396_addImage(r, t); break;
case "shape": t396_addShape(r, t); break;
case "button": t396_addButton(r, t); break;
case "video": t396_addVideo(r, t); break;
case "html": t396_addHtml(r, t); break;
case "tooltip": t396_addTooltip(r, t); break;
case "form": t396_addForm(r, t, e); break;
case "gallery": t396_addGallery(r, t, e); break;
case "vector": t396_addVector(r, t)
}
Ровно десять значений, и ветки default нет: неизвестный тип пропускается
молча, без ошибки в консоли. Значит свой таймер в Zero Block живёт только внутри
элемента html — то есть внутри врезки своего кода.
И вот что про эту врезку стоит знать заранее. t396_addHtml — единственный из
десяти конструкторов, который ничего не инициализирует:
function t396_addHtml(t, e) {
var i = t396_getEl(e);
if (i) {
var r = "width,height,top,left,";
r += "container,axisx,axisy,widthunits,heightunits,leftunits,topunits";
i.setAttribute("data-fields", r), t396_elem__renderView(i)
}
}
Сравните с соседями: у video есть t_onFuncLoad("t396_initVideo", …), у
tooltip — t396_initTooltip. У html — ни одного колбэка, ни одной подписки
на пересчёт, а в списке полей только геометрия: ширина, высота, отступы,
единицы. Никакого heightmode, то есть автовысоты у него нет тоже. Практический
вывод: ваш таймер в Zero Block получит ровно то место, которое вы ему задали
руками, и о показе таба или попапа ничего не узнает.
Часовой пояс: Тильда его измеряет и никуда не применяет
По всему разобранному рантайму getTimezoneOffset вызывается один раз, и
вот где — в tilda-stat-1.0.min.js, при сборке отпечатка посетителя:
e += "cD" + (screen.colorDepth ? screen.colorDepth : "") + "|",
e += "tO" + (new Date).getTimezoneOffset() + "|",
e = c(e += "mT" + (void 0 !== navigator.maxTouchPoints ? … ) + "|")
Date.UTC — 0 вызовов. Date.parse — 0 вызовов. То есть ни одна строка рантайма
не нормализует дату к поясу. Для своего таймера это значит одно: считать надо от
метки времени в UTC, иначе акция «до 23:59» закончится у посетителя из
Владивостока на семь часов раньше, чем вы рассчитывали, — и никто в Тильде за вас
это не поправит.
Ближайший штатный аналог «своё время у каждого» — механизм «не показывать N
дней» из блока T1035. И обратите внимание, где он лежит: не в общем рантайме, а в
постраничном бандле tilda-blocks-page75717.min.js — том самом типе файла, в
котором приезжает и код таймера:
function t1035__initDelayedVisibility(container, thumbCloseBtn) {
var storageItem = container.getAttribute('data-storage-item');
var delta = container.getAttribute('data-storage-delta') * 86400;
var today = Math.floor(Date.now() / 1000);
var lastOpen = null; var currentDelta;
try { lastOpen = localStorage.getItem(storageItem);
currentDelta = today - lastOpen }
catch (e) { console.log('Your web browser does not support localStorage.') }
if (lastOpen === null || currentDelta >= delta)
container.classList.remove('t1035_closed')
}
Разрешение — сутки (* 86400), и всё обёрнуто в try/catch. В режиме инкогнито
и при заблокированном хранилище это просто не работает, и сообщение уходит в
консоль, а не пользователю.
Что часто принимают за таймер, но это не он
Анимация чисел. Её предлагают вместо таймера постоянно, а считает она
вверх и про даты не знает вообще. Вот весь её ход, из
t_animate__animateNumbers:
var p = 12, h = 0,
g = setInterval(function () {
if (u += d / 108, h = c ? … : Math.floor(u) + "", …, u >= d) {
clearInterval(g), t.innerHTML = e; …
}
}, p)
Шаг 12 мс, приращение d / 108 — то есть ровно 108 шагов, примерно 1.3 секунды
от нуля до значения из data-animate-number-count, независимо от самого значения.
Ни дедлайна, ни направления вниз здесь нет и быть не может.
Заодно в той же функции — опечатка, которую видно только на чтении:
n = Array.prototype.filter.call(n, function (t) {
return !t.classList.contains(".t-animate__number") })
classList.contains сравнивает с именем класса, а не с селектором, поэтому
строка с точкой не совпадёт никогда, и этот фильтр не отбрасывает ничего.
Последствие мягкое — стиль для восстановления берётся у первого же span, — но
это ровно та ошибка, которую в своём коде допускают чаще всего.
Полезная вещь, просто не таймер, — разбор с замерами у нас в отдельной модификации.
T142. Его называют блоком таймера, а это блок-кнопка. В CSS:
.t142{width:100%;padding:0 20px;display:flex;justify-content:center;box-sizing:border-box}
— и на живой странице владелец дописывает к нему overflow:visible !important,
чтобы выпустить тень пульсации кнопки. t143, t144, t145 — по нулю везде.
t-countdown. Класса с таким именем в Тильде нет. Поиск по подстроке даёт
три вхождения, и все три сидят внутри имени атрибута:
a = "yes" === o.getAttribute("data-cart-countdown"). Проверка с кавычкой,
[."']t-countdown, даёт ноль.
Что из этого делать
Считайте от метки времени, а не от предыдущего значения. Раз в секунду
вычисляйте разницу между дедлайном в UTC и Date.now(), а не отнимайте единицу
от того, что нарисовано. Тогда таймер не потеряет секунды в фоновой вкладке и
покажет одно и то же всем.
Останавливайтесь, когда вас не видно. Соглашение Тильды здесь правильное, и
проще всего его повторить через IntersectionObserver плюс visibilitychange.
Заодно это снимает нагрузку с телефона.
Не отдавайте CSS половину отсчёта. Если рядом с цифрой есть кольцо, полоса или заливка — двигайте их тем же кодом, который двигает цифру. Иначе получите то, что получилось у самой Тильды: кольцо крутится, цифра стоит.
Учтите цепочку появления. Таймер отодвигает кнопку больше чем на две
секунды, а три кнопки в блоке выворачивают порядок. Если кнопка важна —
переключите блок на ручной порядок через data-animate-order или отключите
анимацию появления у записи.
Откуда числа
t_animate__assignGroupDelay выписана из tilda-animation-2.0.min.js (34.6 КБ)
целиком, вместе с объявлениями всех семи выборок; арифметика в таблице
пересчитана по приращениям a из этого кода. t-timer в этом файле — 1
вхождение при контрольном t-animate в 153. Отсчёт корзины выписан из
tilda-cart-1.1.min.js (188.5 КБ) при контрольном t706 в 312 вхождений,
правило кольца — из tilda-cart-1.0.min.css при контрольном .t706 в 190.
Развилка типов элементов — из t396_artboard_build в tilda-zero-1.1.min.js
(контроль t396 — 486); проверено вторым способом: определений
function t396_add* в файле ровно десять, ни одного лишнего, и список совпадает
со списком case. Правило .t142 — из tilda-blocks-page75717.min.css, единственное
вхождение во всех 37 файлах CSS; overflow:visible !important рядом с ним снят с
живой страницы autolayout-page-mo-ti.ru.html, где он выпускает тень
si-button-pulse. getTimezoneOffset — 1 вызов, Date.UTC — 0, Date.parse —
0, при контрольном Date.now в 163 вызова по тем же 114 файлам.
Две ловушки замера, на которые стоит указать отдельно, потому что каждая даёт
уверенный ложный вывод. t-countdown «встречается» три раза — все три внутри
имени data-cart-countdown; поиск с кавычкой, [."']t-countdown, даёт ноль.
Поиск msk без учёта регистра даёт 23 вхождения в семи файлах — можно решить,
что Тильда где-то приводит время к московскому поясу; все 23 сидят внутри
data-tilda-formskey, а ровно MSK не встречается ни разу.
И общая оговорка. Отсутствие имени в наборе файлов не равно отсутствию его в
Тильде: у Тильды отложенная привязка через t_onFuncLoad (271 вхождение), и код
блока может лежать в постраничном бандле, которого у нас нет. Именно это и
происходит с таймером: t_timer, t_timer__init, t142_init — по нулю, и ни
одно из 71 уникального имени, зарегистрированного через t_onFuncLoad, не
содержит ни timer, ни countdown, ни clock. Всё написанное выше — про код,
который есть, и не распространяется на код, которого нет.