Calendário animado usando HTML + CSS + JS

Oct 17 2020

Sou um desenvolvedor iniciante e estou construindo um calendário para smartphones. Estou usando HTML, CSS e JS. Ainda não terminei totalmente o projeto, no entanto, tenho a sensação de que estou criando um código confuso . Estarei postando o link para o git + o código aqui, para que você possa lê-lo / clonar melhor, e me ajudar com padrões de design, etc ...

PS: O projeto foi desenvolvido na resolução IPhoneX (375 x 812). Ainda não está respondendo, então eu recomendo executar o código usando a resolução acima.

Link Git: https://github.com/LucasBiazi/Mobile_Calendar_HTML_CSS_JS

HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Calendar</title>
    <script src="../script/script.js"></script>
    <link rel="stylesheet" href="../css/style.css" />
  </head>
  <!-- As soon as the page loads, fulfills the table. -->
  <body onload="load_DB(new Date().getFullYear(), new Date().getMonth())">
    <div class="main">
      <div class="title">
        <span class="year_title" id="year_title"></span>
        <span class="month_title" id="month_title"></span>
      </div>
      <div class="calendar">
        <div id="month_days" class="month_days">
          <table id="days">
            <tr>
              <th style="color: lightsalmon">Sun</th>
              <th class="even">Mon</th>
              <th>Tue</th>
              <th class="even">Wed</th>
              <th>Thu</th>
              <th class="even">Fri</th>
              <th style="color: lightsalmon">Sat</th>
            </tr>
          </table>
        </div>
        <!-- Future implementations. -->
        <div class="data">
          <div class="data_content">
            <p style="color: black; font-size: 20px">No content.</p>
          </div>
        </div>
      </div>
      <div class="buttons">
        <!-- Reteats a month. -->
        <button
          style="
            animation: display_button_back 1s ease;
            position: relative;
          "
          onclick="retreat_month(parseInt(document.getElementById('year_title').textContent), 
        identify_month(document.getElementById('month_title').textContent))"
        >
          <img
            src="../../media/left-arrow-line-symbol.svg"
            style="width: 35px; height: 35px"
            alt="back"
          />
        </button>
        <!-- Shows current month. -->
        <button
          style="animation: display_opacity_zoom 1s ease-out;"
          onclick="advance_month(new Date().getFullYear(), new Date().getMonth() - 1)"
        >
          T
        </button>
        <!-- Advances a month. -->
        <button
          style="
            animation: display_button_next 1s ease;
            position: relative;
          "
          onclick="advance_month(parseInt(document.getElementById('year_title').textContent), 
          identify_month(document.getElementById('month_title').textContent))"
        >
          <img
            src="../../media/right-arrow-angle.svg"
            style="width: 35px; height: 35px"
            alt="next"
          />
        </button>
      </div>
    </div>
  </body>
</html>

CSS:

* {
  padding: 0px;
  margin: 0px;
  font-family: Verdana, Geneva, Tahoma, sans-serif;
  font-weight: lighter;
}

body {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: url(../../media/grass.jpg);
  /* Blurring the background. Applies behind the element... */
  backdrop-filter: blur(9px);
  background-size: cover;
}

@keyframes display_data {
  0% {
    transform: scale3d(0.25, 0, 1);
  }
  25% {
    transform: scale3d(0.5, 0, 1);
  }
  50% {
    transform: scale3d(0.1, 0, 1);
  }
  75% {
    transform: scale3d(0.1, 1.2, 1);
  }
  100% {
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_month_days {
  from {
    opacity: 0%;
  }
  to {
    opacity: 100%;
  }
}
@keyframes display_button_back {
  0% {
    right: 25px;
    transform: scale3d(0.75, 0.75, 1);
  }
  100% {
    right: 0px;
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_button_next {
  0% {
    left: 25px;
    transform: scale3d(0.75, 0.75, 1);
  }
  100% {
    left: 0px;
    transform: scale3d(1, 1, 1);
  }
}
@keyframes display_opacity_zoom {
  from {
    opacity: 0%;
    transform: scale3d(0.5, 0.5, 1);
  }
  to {
    opacity: 100%;
    transform: scale3d(1, 1, 1);
  }
}
.main {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  color: white;
  background-color: rgba(0, 0, 0, 0.65);
}

.title {
  margin-top: 7%;
  height: 80px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
  animation: display_opacity_zoom 1s ease-out;
}

.year_title {
  margin-left: 5px;
  font-size: 40px;
  letter-spacing: 5px;
  color: lightsalmon;
  text-align: center;
}

.month_title {
  margin-left: 15px;
  font-size: 25px;
  letter-spacing: 15px;
  text-align: center;
}

.calendar {
  height: 75%;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
}

.month_days {
  margin-top: 10px;
  width: 100%;
  height: 50%;
  animation: display_month_days 1s ease-in-out;
}

table {
  margin-top: 20px;
  width: 100%;
  font-size: 22px;
}

tr,
th,
td {
  background-color: none;
}

th {
  width: 14%;
  text-align: center;
  color: white;
}

td {
  width: 2.38em;
  height: 2.38em;
  color: white;
  text-align: center;
  border-radius: 50%;
}

td:hover {
  background-color: black;
}

.data {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 95%;
  height: 30%;
  background-color: rgba(255, 255, 255, 0.9);
  border: none;
  border-radius: 5px;
  animation: display_data 0.8s ease;
}

.data_content {
  width: 95%;
  height: 95%;
}

.other_month {
  background: none;
  color: rgba(175, 175, 175, 0.45);
}

.buttons {
  width: 100vw;
  height: 70px;
  display: flex;
  justify-content: space-around;
  align-items: flex-start;
}

button {
  width: 60px;
  height: 60px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: none;
  border: none;
  font-size: 35px;
  font-weight: 400;
  color: white;
}

JS:

// Returns the day of the week in which the month starts.
starting_day = (year, month) => new Date(year, month, 1).getDay();

// Returns the amount of days in a month.
total_month_days = (year, month) => new Date(year, month + 1, 0).getDate();

// Set the background color of the cell that contains today's day.
function is_today(months, cell) {
  if (
    new Date().getFullYear() ==
      parseInt(document.getElementById("year_title").textContent) &&
    months[new Date().getMonth()].month ==
      document.getElementById("month_title").textContent &&
    cell.textContent == new Date().getDate()
  ) {
    cell.style.background = "rgba(255, 160, 122, 0.8)";
  }
}

// Changes color if it is a weekend.
color_weekend = (let_value, cell) => {
  if (let_value == 6 || let_value == 0) cell.style.color = "lightsalmon";
};

// Populates the DB.
function load_DB(year, month) {
  let counter = 1;
  const table = document.getElementById("days");
  const date_object = new Date(year, month);

  // Array containing months names, starting_day()_+ total_month_days().
  const months = [
    {
      // Month 0
      month: "January",
      first_day: starting_day(date_object.getFullYear(), 0),
      days: Array(total_month_days(date_object.getFullYear(), 0)),
    },
    {
      // Month 1
      month: "February",
      first_day: starting_day(date_object.getFullYear(), 1),
      days: Array(total_month_days(date_object.getFullYear(), 1)),
    },
    {
      // Month 2
      month: "March",
      first_day: starting_day(date_object.getFullYear(), 2),
      days: Array(total_month_days(date_object.getFullYear(), 2)),
    },
    {
      // Month 3
      month: "April",
      first_day: starting_day(date_object.getFullYear(), 3),
      days: Array(total_month_days(date_object.getFullYear(), 3)),
    },
    {
      // Month 4
      month: "May",
      first_day: starting_day(date_object.getFullYear(), 4),
      days: Array(total_month_days(date_object.getFullYear(), 4)),
    },
    {
      // Month 5
      month: "June",
      first_day: starting_day(date_object.getFullYear(), 5),
      days: Array(total_month_days(date_object.getFullYear(), 5)),
    },
    {
      // Month 6
      month: "July",
      first_day: starting_day(date_object.getFullYear(), 6),
      days: Array(total_month_days(date_object.getFullYear(), 6)),
    },
    {
      // Month 7
      month: "August",
      first_day: starting_day(date_object.getFullYear(), 7),
      days: Array(total_month_days(date_object.getFullYear(), 7)),
    },
    {
      // Month 8
      month: "September",
      first_day: starting_day(date_object.getFullYear(), 8),
      days: Array(total_month_days(date_object.getFullYear(), 8)),
    },
    {
      // Month 9
      month: "October",
      first_day: starting_day(date_object.getFullYear(), 9),
      days: Array(total_month_days(date_object.getFullYear(), 9)),
    },
    {
      // Month 10
      month: "November",
      first_day: starting_day(date_object.getFullYear(), 10),
      days: Array(total_month_days(date_object.getFullYear(), 10)),
    },
    {
      // Month 11
      month: "December",
      first_day: starting_day(date_object.getFullYear(), 11),
      days: Array(total_month_days(date_object.getFullYear(), 11)),
    },
  ];

  // Prints the name of the current month and year.
  document.getElementById("year_title").textContent = date_object.getFullYear();
  document.getElementById("month_title").textContent =
    months[date_object.getMonth()].month;

  // Month days that will appear in the first row.
  const number_of_cells_in_the_first_row =
    7 - months[date_object.getMonth()].first_day;

  // Month days that will appear after the first row.
  let normal_days = number_of_cells_in_the_first_row + 1;

  // Creates + populates the 5 last rows.
  for (let r = 0; r < 5; r++) {
    let row = table.insertRow(r + 1);
    let cell;
    // Creates + populates 7 cells in each row.
    for (let x = 0; x < 7; x++) {
      cell = row.insertCell(x);
      cell.textContent = normal_days;
      is_today(months, cell);
      color_weekend(x, cell);
      normal_days++;
      // Filling the rest of the table with the days of the next month(gray days).
      if (normal_days > months[date_object.getMonth()].days.length + 1) {
        // Next month days are always lowlighted.
        if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";
        cell.textContent = counter++;
        cell.className = "other_month";
      }
    }
  }

  // Creates + populates the 1 row.
  for (let i = 0; i < 1; i++) {
    let row = table.insertRow(i + 1);
    let cell;
    let number_of_blank_cells = 7 - number_of_cells_in_the_first_row;
    // Populates it.
    for (let y = 0; y < number_of_cells_in_the_first_row; y++) {
      cell = row.insertCell(0);
      cell.textContent = number_of_cells_in_the_first_row - y;
      is_today(months, cell);
      color_weekend(y, cell);
    }
    // Filling the rest of the table (next month days).
    for (let w = 0; w < number_of_blank_cells; w++) {
      cell = row.insertCell(0);
      if (months[date_object.getMonth() - 1]) {
        cell.textContent = months[date_object.getMonth() - 1].days.length--;
        cell.className = "other_month";
      } else {
        cell.textContent = months[date_object.getMonth() + 11].days.length--;
        cell.className = "other_month";
      }
    }
  }
}

// Converts the month name to the month number (January = 0).
function identify_month(string) {
  const all_months = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];
  let i = 0;
  while (string != all_months[i]) {
    i++;
  }
  return i;
}

// Advancecs a month.
function advance_month(year, month) {
  // Cleaning table.
  const table = document.getElementById("days");
  for (let i = 0; i < 6; i++) {
    table.deleteRow(1);
  }
  // Add new data.
  month++;
  load_DB(year, month);
}

// Retreats a month.
function retreat_month(year, month) {
  // Cleaning table.
  const table = document.getElementById("days");
  for (let i = 0; i < 6; i++) {
    table.deleteRow(1);
  }
  month--; // Add new data.
  load_DB(year, month);
}

Respostas

6 SᴀᴍOnᴇᴌᴀ Oct 17 2020 at 12:28

Bem-vindo à revisão do código. O CSS parece muito bom. Não vi muitas folhas de estilo com a cor lightsalmon😃. Abaixo estão algumas sugestões.

Referências DOM do cache

”... o acesso ao DOM é realmente muito caro - penso nisso como se eu tivesse uma ponte - como dois pedaços de terra com uma ponte com pedágio, e o mecanismo JavaScript está de um lado, e o DOM está do outro, e toda vez que eu quiser acessar o DOM do mecanismo JavaScript, tenho que pagar esse pedágio ”
    - John Hrvatin, Microsoft, MIX09, nesta palestra Construindo aplicativos e sites de alto desempenho às 29:38, também citada no O'Reilly Livro Javascript de Nicholas C Zakas Pg 36 , assim como mencionado neste post

Tendo em mente que foi afirmado há mais de 10 anos e os navegadores provavelmente têm evoluído desde então, ainda é aconselhável estar ciente disso. Armazene referências a elementos DOM - por exemplo, document.getElementById("year_title")em variáveis ​​- isso pode acontecer em um retorno de chamada para o DOMContentLoadedevento .

let titleEl;
window.addEventListener('DOMContentLoaded', (event) => {
    titleEl = document.getElementById("year_title");
});

Como Joshua mencionou, os manipuladores de eventos em HTML são malvistos pelos padrões de hoje, e esse retorno de chamada seria um lugar apropriado para ter o código atualmente no onloadatributo do elemento body.

nomes dos meses

Como Joshua já mencionou, o código para os meses é bastante repetitivo. Em vez de armazenar os nomes dos meses, os nomes dos meses Date.prototype.toLocaleDateString()podem ser gerados dinamicamente, possivelmente em um idioma especificado pelo usuário e / ou navigator.languagese estiver disponível.

Nome de função mais legível

// Set the background color of the cell that contains today's day.
function is_today(months, cell) {

Julgando pelo nome, eu acho que a função retornaria um booleano que indica se algum valor é hoje, embora, dado o comentário e a implementação, esse não seja o caso. Um nome mais apropriado pode ser style_todays_date_cell.

Igualdade estrita

É um bom hábito usar operadores de comparação estritos - ou seja, ===e !==quando possível - por exemplo, para esta comparação dentro de load_DB():

if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";

Incremento de pré-correção

Estas linhas:

month++;
load_DB(year, month);

Pode ser combinado quando o valor de monthé incrementado antes da chamada usando o operador de incremento de prefixo :

 load_DB(year, ++month);

estilos inline redundantes -> CSS

As imagens nos botões têm estilos embutidos, ou seja,

 style="width: 35px; height: 35px"

Isso pode ser movido para a folha de estilo:

button img {
    width: 35px;
    height: 35px;
}

Estilos embutidos para cabeçalhos de tabela

A primeira e a última células têm estilos embutidos para substituir a thregra padrão :

<th style="color: lightsalmon">Sun</th>

Isso também pode ser movido para a folha de estilo usando seletores mais específicos - por exemplo, pseudo-seletores :first-childe :last-child:

th:first-child,
th:last-child {
    color: lightsalmon;
}
          

As alternativas incluem :first-of-typee :last-of-typeEssa técnica também pode ser usada para mover a linha acima para fora do JavaScript e para a folha de estilo usando em tdvez de tr:

if (x == 6 || x == 0) cell.style.color = "rgba(175, 175, 175, 0.45)";

PS: A resposta foi composta usando o IPhone VII +

6 JoshuaHall Oct 17 2020 at 04:42

Variáveis

Sempre use uma declaração de variável ao declarar sua variável pela primeira vez. Do contrário, sua variável será colocada no escopo global , que quase nunca é o que você deseja. Também torna mais claro que você não deseja reatribuir a uma variável global já existente.

Assim, para starting_day, por exemplo, colocar um var, letou constna frente dele.

Atributos HTML

Em vez de usar atributos como onclickou onloadpara manipular eventos (também conhecidos como manipuladores embutidos), geralmente é considerada a prática recomendada manipular esses eventos em seu próprio JavaScript. Isso ajuda a separar seu HTML / JavaScript.

Por exemplo, para substituir o onloadatributo, você pode usar a seguinte função JS:

function ready(func) {
  if (document.readyState !== 'loading') {
    func();
  } else {
    document.addEventListener('DOMContentLoaded', func);
  }
}

Você poderia declarar uma função chamada mainpor exemplo, onde colocaria qualquer código que deseja executar quando o DOM estivesse pronto para ser trabalhado e, em seguida, chamaria ready(main);.

Para substituir os onclickatributos, você deve adicionar algumas classes / ids CSS a eles para serem usados ​​como seletores no código JavaScript. Em seguida, você os selecionaria usando algo semelhante const backButton = document.querySelector('calendarBack');. Em seguida, você pode anexar um ouvinte de evento a esse elemento, assim:

backButton.addEventListener("click", () => {
   // The code you want to run when the button is clicked goes here
});

Refatorações de código

months

A monthsdeclaração da variável é bastante repetitiva e há apenas uma coisa mudando a cada mês, o 'índice' dela. Isso o torna um candidato perfeito para rolar em um mapa de loop / array.

Eu faria assim usando a mapfunção :

// This would ideally not be in load_DB but be a top level declaration, since you could then reference this again in the identify_month function instead of rewriting the months
const monthNames = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

// Saving this in a variable for brevity and since it doesn't change
const fullYear = date_object.getFullYear();

const months = monthNames.map((monthName, index) => {
  return {
    month: monthName,
    first_day: starting_day(fullYear, index),
    days: Array(total_month_days(fullYear, index)),
  };
});

Uma coisa da qual não tenho certeza é a dayspropriedade e por que ela é uma matriz para começar. Os únicos lugares onde ele está sendo referenciado no código é para verificar seu comprimento. Isso é parte do código inacabado que você mencionou? Se não for, eu apenas substituiria a linha por dias: em Array(total_month_days(fullYear, index)),vez disso.

identify_month

A implementação original desta função deve funcionar, embora seja uma longa maneira de dizer "obter o índice do mês do monthNamesarray".

Aqui está como eu o reescreveria:

// Converts the month name to the month number (January = 0).
function identify_month(string) {
  return monthNames.indexOf(string);
}

Código do mês atual

O código original é bastante difícil de ler por causa das pesquisas repetidas do objeto mês encontrado na monthsmatriz. Coloque-os em uma variável para melhorar a legibilidade e remover acessos adicionais desnecessários à matriz.

const currentMonth = date_object.getMonth();

const currentMonthObj = months[currentMonth];

// Prints the name of the current month and year.
document.getElementById("year_title").textContent = fullYear;
document.getElementById("month_title").textContent = currentMonthObj.month;

// Month days that will appear in the first row.
const number_of_cells_in_the_first_row = 7 - currentMonthObj.first_day;

Vou tentar voltar e adicionar mais alguns pensamentos mais tarde :)