JS30. Задание 29 Countdown Timer


Демо: https://js30291.github.io/
Код: https://github.com/js30291/js30291.github.io

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

Разметка

<div class="timer">
    <div class="timer__controls">
        <button data-time="20" class="timer__button">20 Secs</button>
        <button data-time="300" class="timer__button">Work 5</button>        <button data-time="900" class="timer__button">Quick 15</button>        <button data-time="1200" class="timer__button">Snack 20</button>        <button data-time="3600" class="timer__button">Lunch Break</button>        <form name="customForm" id="custom">
            <input type="text" name="minutes" placeholder="Enter Minutes">
        </form>
    </div>
    <div class="display">
        <h1 class="display__time-left"></h1>
        <p class="display__end-time"></p>
    </div>

</div>

HTML состоит из контейнера div. Внутри этого есть два div. Первый div предназначен для элементов управления, который включает в себя пять кнопок и форму / ввод. Второй div используется для отображения результатов ввода. Существует тег h1 для таймера и тег абзаца для времени возврата.

Стили

Стилизация на флексбоксах. То есть, в IE этот проект работать не будет. Зато будет работать в Edge. И ещё у автора есть курс по флексбоксам - 20 бесплатных видеоуроков https://flexbox.io/

JS-код

Создадим функцию timer, принимающую в качестве аргумента значение временного интервала в секундах

function timer(seconds) {
    



Передадим этой функции значение текущего времени now
Раньше для этой цели нам понадобилось бы написать что-то вроде

var now = (new Date()).getTime();

Сейчас есть новый метод для получения текущего времени

var now = Date.now();

Таким образом мы получим значение текущего времени в миллисекундах, прошедших с 1 января 1970 года

Создадим ещё одну переменную then, которая покажет нам время после того, как пройдёт заданный временной интервал

var then = now + seconds * 1000

Умножение на 1000 понадобилось, чтобы перевести миллисекунды в секунды

Выясним, сколько времени осталось до наступления момента времени then

var seconsLeft = (then - Date.now()) / 1000;

Округлим полученное значение до ближайшего целого

var secondsLeft = Math.round((then - Date.now()) / 1000);

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

setInterval(() => {
        var secondsLeft = Math.round((then - Date.now()) / 1000);
        console.log(seconsLeft);
    }, 1000);


Трюк с использованием setInterval и стрелочной функции => рассматривали в предыдущей теме. Дело в том, что обычная функция в качестве контекста вызова выведет объект window, в котором создана функция timer. У стрелочной функции своего собственного лексического окружения нет и она получает его из той функции, внутри которой была создана. Ну, это если я ничего не напутала, потому что сомнения есть.

Запустим функцию

function timer(seconds) {   
  var now = Date.now();
  var then = now + seconds * 1000;
  setInterval(() => {
        var secondsLeft = Math.round((then - Date.now()) / 1000);
        console.log(secondsLeft);
    }, 1000); 
}
timer(5)


Результат


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

function timer(seconds) {   
  var now = Date.now();
  var then = now + seconds * 1000;
  setInterval(() => {
        var secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft <= 0) return;
        console.log(secondsLeft);
    }, 1000); 
}
timer(5)


Работает )
Но нуль не выводится.
Меняем условие:  если значение secondsLeft меньше нуля - return
Но значение setInterval не сбросилось и, если запускать функцию ещё раз, могут быт ошибки

Теперь бы нам как-то научиться сбрасывать  setInterval()
Например, если мы хотим запустить второй таймер, пока не закончился первый

Для этого есть метод clearInterval();
К сожалению, по спецификации он вызывается с идентификатором того setInterval(), который должен остановить clearInterval(intervalID) 
В общем, это понятно и разумно. Но заставляет нас усложнить код. Необходимо создать глобальную переменную, присвоить её setInterval() и указывать её в качестве аргумента clearInterval();

var countdown;

function timer(seconds) {
   clearInterval(countdown);

    var now = Date.now();
    var then = now + seconds * 1000;


    displayTimeLeft(seconds);
    displayEndTime(then);

    countdown = setInterval(() => {
        var secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft < 0) {
            clearInterval(countdown);
            return;
        }


        displayTimeLeft(secondsLeft);
    }, 1000);
}


Здесь в коде  function timer(seconds) вызываются ещё две функции displayTimeLeft(); и displayEndTime();
Они нужны, чтобы вывести результаты данной функции на экран.

Функция displayEndTime будет запрашиваться один раз. Её задача вывести на экран время, оставшееся до окончания указанного интервала
По сути это электронные часы, которые я добавляла во втором задании, но в авторском испонении они значительно короче и элегантнее

var endTime = document.querySelector(".display__end-time");

function displayEndTime(timestamp) {
    var end = new Date(timestamp);
    var hour = end.getHours();
    var adjustedHour = hour > 12 ? hour - 12 : hour;
    var minutes = end.getMinutes();
    endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? "0" : ""}${minutes}`;
}


Функцию displayTimeLeft , которая ведёт отсчёт оставшегося до конца интервала времени, мы вызываем  дважды. Первый раз она получит и выведет на экран продолжительность интервала в секундах, во второй - результат выполнения функции, осуществляющей обратный отсчёт времени.

Код функции

var timerDisplay = document.querySelector(".display__time-left");

function displayTimeLeft(seconds) {
    var minutes = Math.floor(seconds / 60);
    var remainderSeconds = seconds % 60;
    var display = `${minutes}:${remainderSeconds < 10 ? "0" : "" }${remainderSeconds}`;
    document.title = display;
    timerDisplay.textContent = display;
}

Строка document.title = display; означает, что имя документа будет меняться в соответствии с оставшимся до конца интервала временем и отображаться в заголовке вкладки.

Теперь, когда у нас установлены наши функции времени и отображения, нам нужно добавить функциональность для кнопок.

Для всех кнопок создаём одну переменную

var buttons = document.querySelectorAll("[data-time]");

В атрибуте data-time каждой кнопки указано время в секундах. Его можно получить при помози метода dataset, перевести в число при помощи унарного плюса или метода parseInt() и передать в функцию timer(seconds)значение времени в секундах.

function startTimer() {
    var seconds = parseInt(this.dataset.time);
    timer(seconds);
}


Функция startTimer будет запускаться при клике по кнопке:

buttons.forEach(button => button.addEventListener("click", startTimer));

Теперь мы делаем то же самое для поля ввода.

document.customForm.addEventListener("submit", function(e) {
    e.preventDefault();
    var mins = this.minutes.value;
    console.log(mins);
    timer(mins * 60);
    this.reset();
});  


Для поля ввода мы находим имя формы в документе - document.customForm - и добавляем слушиватель события submit, который запускает анонимную функцию function(e).

Метод event.preventDefault - отмена события по умолчанию - отменяет отправку содержимого формы на сервер.

Затем мы назначаем переменную mins равную this.minutes.value - this является элементом формы, minutes -  имя поля ввода, а value - значение поля ввода, указанное пользователем.

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

Последнее, что мы делаем, это запустить this.reset, чтобы очистить ввод после того, как значение было введено.

Код полностью:

var countdown;
var timerDisplay = document.querySelector(".display__time-left");
var endTime = document.querySelector(".display__end-time");
var buttons = document.querySelectorAll("[data-time]");

function timer(seconds) {
    // clear any existing timers
    clearInterval(countdown);

    var now = Date.now();
    var then = now + seconds * 1000;
    displayTimeLeft(seconds);
    displayEndTime(then);

    countdown = setInterval(() => {
        var secondsLeft = Math.round((then - Date.now()) / 1000);
        // check if we should stop it!
        if(secondsLeft < 0) {
            clearInterval(countdown);
            return;
        }
        // display it
        displayTimeLeft(secondsLeft);
    }, 1000);
}

function displayTimeLeft(seconds) {
    var minutes = Math.floor(seconds / 60);
    var remainderSeconds = seconds % 60;
    var display = `${minutes}:${remainderSeconds < 10 ? "0" : "" }${remainderSeconds}`;
    document.title = display;
    timerDisplay.textContent = display;
}

function displayEndTime(timestamp) {
    var end = new Date(timestamp);
    var hour = end.getHours();
    var adjustedHour = hour > 12 ? hour - 12 : hour;
    var minutes = end.getMinutes();
    endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? "0" : ""}${minutes}`;
}

function startTimer() {
    var seconds = parseInt(this.dataset.time);
    timer(seconds);
}

buttons.forEach(button => button.addEventListener("click", startTimer));

document.customForm.addEventListener("submit", function(e) {
    e.preventDefault();
    var mins = this.minutes.value;
    console.log(mins);
    timer(mins * 60);
    this.reset();
});