Делаем свой JS календарь

Понадобился мне javascript виджет с выбором даты. И так как я не люблю тащить километровые зависимости, я решил написать свой собственный велосипед.

Для начала делаем обычную хтмльку, подключаем в неё бутстрап и добавляем кнопку активации календаря и пару тестовых инпутов, куда будем выводить результат:

<html>
  <head>
    <title>Date picker on pure js</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
    <style>
      /* datepicker styles */
      
      .datetimepicker-header {
        position: relative;
        background: #f00;
      }
      
      .datetimepicker-header-close {
        position: absolute;
        right: 1px;
        top: 1px;
        margin: 0;
        padding: 0;
        color: #fff;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
  
  <div class="container">
    <h1>Datetime picker test</h1>
    <form>
      <input type="text" id="dateInput" class="form-control"/>
      <span id="datePickerWrapper"></span><a href="#" class="btn btn-primary" onclick="showCalend(this)">Open calendar</a>
    </form>
  </div>
  </body>
</html>

После этого, нам нужно написать функцию, которая будет генерировать сам календарь, на вход она будет получать год и месяц, а возвращать будет строку, с хтмл кодом уже готового календаря:


  function generateCalendar(month, year) {
    let result = '';
    let curTime = new Date();
    let months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
    
    result += '<div class="datetimepicker-wrapper">';
    result += '<div class="datetimepicker-header">';
    result += '<h1 class="datetimepicker-title">Select date</h1>';
    result += '<h1 class="datetimepicker-header-close" onclick="closeDatePicker(this)">X</h1>';
    result += '</div>';
    result += '<div class="datetimepicker-body">';
    result += '<div class="row">';
    result += '<div class="col">';
    result += '<select class="form-control datetimepicker-month" onchange="datePickerMonthChange(this)">';
    
    for(let index in months)
      if( index == month )
        result += `<option value="${index}" selected>${months[ index ]}</option>`;
      else
        result += `<option value="${index}">${months[ index ]}</option>`;

    result += '</select>';
    result += '</div>';
    result += '<div class="col">';
    result += '<input type="number" class="form-control datetimepicker-year" value="' + year + '" onchange="datePickerYearChange(this)"/>';
    result += '</div>';
    result += '</div>';
    result += '<div class="row"><div class="col-sm-12">';
    result += '<table class="table"><tbody class="datetimepicker-datetablebody"></tbody></table>';
    result += '</div></div>';
    result += '</div>';
    result += '</div>';
    result += '';
    
    return result;
  }

Сейчас наш календарь буддет выглядеть так:

У нас есть выбор месяца, выбор года, нужно сделать функцию которая будет генерировать таблицу с выбором даты:

  function generateDateTableBody(month, year) {
    let result = '';
    let counter = 0;
    let curDate = new Date(year, month, 1, 0, 0, 0, 0);    
    let startDatOffset = curDate.getDay() * -1 + 2;
    
    for(let week=0; week<6; week++) {
      result += '<tr>';
      
      for(let day=0; day<7; day++) {
        
        let d = new Date(year, month, counter + startDatOffset, 0, 0, 0, 0);        
        if( d.getMonth() == month )
          result += `<td class="datetimepicker-curmonth" data-month="${d.getMonth()}" data-year="${d.getFullYear()}" onclick="dayClick(this)">${d.getDate()}</td>`;
        else 
          result += `<td class="datetimepicker-not-curmonth" data-month="${d.getMonth()}" data-year="${d.getFullYear()}" onclick="dayClick(this)">${d.getDate()}</td>`;
        
        counter ++;
      }
      
      result += '</tr>';
    }
    
    return result;
  }

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

      .datetimepicker-curmonth {
        text-align: center;
        cursor: pointer;
        background: #ddd;
        font-weight: bold;
      }
      
      .datetimepicker-curmonth:hover {
        background: #ccf;
      }
      
      .datetimepicker-not-curmonth {
        text-align: center;
        cursor: pointer;
        background: #eee;
      }
      
      .datetimepicker-not-curmonth:hover {
        background: #ccf;
      }

Подключаем эту функцию в closeDatePicker:

...
result += '<table class="table"><tbody class="datetimepicker-datetablebody">' + generateDateTableBody(month, year) + '</tbody></table>';
...

И получаем уже почти готовый календарь:

Нам осталось сделать обработчики событий. Для выбора месяца и выбора года:

  function datePickerMonthChange(e) {
    let month = e.options[e.selectedIndex].value;
    let wrapper = e.closest(`.datetimepicker-wrapper`);
    let tableBody = wrapper.getElementsByClassName('datetimepicker-datetablebody')[ 0 ];
    let yearSelector = wrapper.getElementsByClassName('datetimepicker-year')[ 0 ];
    tableBody.innerHTML = generateDateTableBody(parseInt(month), parseInt(yearSelector.value));
  }
  
  function datePickerYearChange(e) {
    let year = parseInt(e.value);
    let wrapper = e.closest(`.datetimepicker-wrapper`);
    let tableBody = wrapper.getElementsByClassName('datetimepicker-datetablebody')[ 0 ];
    let monthSelector = wrapper.getElementsByClassName('datetimepicker-month')[ 0 ];
    tableBody.innerHTML =generateDateTableBody(parseInt(monthSelector.options[monthSelector.selectedIndex].value), year);
  }

Работают они так. datePickerYearChange получает ссылку на дом элемент, ``input type="number" class="form-control datetimepicker-year"`` в котором произошло событие. Далее имея доступ к элементу, котоый находится внутри календаря, можем найти основного родителя, в котром находится календарь, с помощью

let wrapper = e.closest(`.datetimepicker-wrapper`);

И уже имея доступ к родителю, с помощью getElementsByClassName находим место куда нужно воткнуть перегенерированный календарь и инпут с месяцем. Ровно то же самое происходит в datePickerMonthChange

И добавляем последний обработчик события, уже клик по дню месяца, в табличке:

  function dayClick(e) {
    let wrapper = e.closest(`.datetimepicker-wrapper`);
    let curDate = new Date(parseInt(e.dataset.year), parseInt(e.dataset.month), parseInt(e.innerHTML), 0, 0, 0, 0);
    wrapper.parentNode.selectCB(curDate);
    wrapper.remove();
  }

wrapper.parentNode.selectCB(curDate); сейчас не сработает, нам нужно ещё написать функцию, которая активирует календарь:

  function datePicker(wrapper, cb, startDate=null) {
    let curDate = new Date();
    
    if( startDate !== null )
      curDate = new Date(startDate);
    
    wrapper.innerHTML = generateCalendar(curDate.getMonth(), curDate.getFullYear());
    wrapper.selectCB = cb;
  }

На вход она получает дом элемент куда нужно воткнуть календарь, второй параметр- это функция колбэк, которая сработает, когда пользователь выберит дату. Эту функцию, мы сохраняем в родительский элемент ``wrapper.selectCB = cb``. И в dayClick у нас будет доступен этот колбэк.

В завершении добавляем обработчик кнопки закрытия календаря и активации календаря:

  function closeDatePicker(e) {
    e.closest(`.datetimepicker-wrapper`).remove();
  }

  function calendarCB(date) {
    document.getElementById('dateInput').value = date;
  }
  
  function showCalend() {
    datePicker(document.getElementById('datePickerWrapper'), calendarCB);
  }

Готовый пример можно посмотреть тут. Ну а т.к тут весь проект - это всего 1 файл, то гитхаба не будет, нет смысла.

Бонусом видео, как оно делалось:



Путеводитель по Батуми

Всем привет. Сегодня я хочу рассказать о нашем опыте жизни в Батуми, а прожили мы в нём 2 раза по пол года.


(0) Комментариев