Jaime Neto | desenvolvimento para web

fev/12

17

Calendário para Zend Framework


Precisei criar um calendário HTML dinâmico, e depois de pesquisar bastante, e achar várias soluções que não resolviam todas as minha necessidades, resolvi criar eu mesmo uma classe para isso. Ou melhor, um View Helper para Zend Framework. Assim, eu poderia usar em vários outros projetos, diante da necessidade. Só que para ele poder realmente ser bem aproveitado, precisaria ser fácil de customizar, tanto visualmente, quanto, com relação à sua funcionalidade.

Pensei então nas possíveis utilizações para uma classe dessas, e decidi que ela precisava:

  1. ser facilmente alterada, visualmente, apenas mexendo no CSS;
  2. receber e exibir diferentes formatos de data;
  3. ter opções de navegação, para se alterar o mês e ano exibidos;
  4. ter datas selecionáveis, para que se altere algo mais na página;
  5. poder, de alguma forma, receber conteúdo específico para cada data, como nome dos feriados, por exemplo.

Para resolver o primeiro ponto, decidi que a classe deveria imprimir o HTML com várias classes CSS para cada possível caso de alteração visual que possa ser necessário. Ou seja, classes que diferenciem:

  • cada dia da semana;
  • dias da semana e dias de fim de semana;
  • dia atual, dias passados e dias futuros;
  • dia selecionado pelo usuário (ponto 4);
  • dias de outro mês, mês passado e mês futuro;
  • dias definidos pelo usuário.

O segundo ponto diz respeito a como vai vir a informação que define a data selecionada, por exemplo, e as datas que o usuário irá especificar classes de css (ultimo da lista do ponto 1) e conteúdo específico (ponto 5), e também de como serão exibidos os nomes do mês em exibição, dos meses nas opções de navegação (ponto 3), e os dias da semana.

Os outros pontos são auto-explicativos. Então, vamos ver o resultado:

class My_View_Helper_Calendar extends Zend_View_Helper_Abstract
{
    protected $date;
    protected $now;
    protected $locale = 'pt_BR';
 
    const INSERT_BEFORE = -1;
    const INSERT_PREPEND = 0;
    const INSERT_APPEND = 1;
    const INSERT_AFTER = 2;
 
    protected $options = array(
        'showPrevMonthLink'  => false,
        'showNextMonthLink'  => false,
        'showOtherMonthDays' => false,
        'dateBaseUrl'        => '#',
        'monthBaseUrl'       => '#',
        'insertsPosition'    => -1
    );
 
    protected $protectedCssClasses = array(
        'selected-date', 'today', 'past-day', 'future-day',
        'other-mohth', 'prev-month', 'next-month'
    );
 
    protected $cssClasses = array();
    protected $inserts = array();
 
    static protected $formats = array(
        'input'    => 'yyyy-MM-dd',
        'weekdays' => Zend_Date::WEEKDAY_NARROW,
        'header'   => 'MMMM yyyy',
        'prevAndNextMonth' => Zend_Date::MONTH_NAME_SHORT
    );
 
    static public function getFormat($name)
    {
        return isset(self::$formats[$name])
                ? self::$formats[$name]
                : null;
    }
 
    /**
     * Set object state from options array
     *
     * @param array $options
     * @return My_View_Helper_Calendar
     */
    public function setOptions(array $options)
    {
        foreach($options as $name => $value)
        {
            // Define o formato que a data será inserida
            if ($name == 'format') {
                self::$formats['input'] = $value;
            }
 
            // Define o idioma
            if ($name == 'locale' && Zend_Locale::isLocale($value)) {
                $this->locale = $value;
            }
 
            // Define as classes de css
            if ($name == 'cssClasses') {
                $this->setCssClasses($value);
            }
 
            // Insere conteúdo nas células de acordo com as datas
            if ($name == 'inserts') {
                $this->insertHtml($value);
            }
 
            // Define a posição em que o conteúdo será inserido
            if ($name == 'insertsPosition') {
                $this->setInsertsPosition($value);
            }
 
            $this->setOption($name, $value);
        }
        return $this;
    }
 
    /**
     * Altera alguma configuração do calendário
     *
     * @param string $name
     * @param mixed $value
     * @return My_View_Helper_Calendar
     */
    public function setOption($name, $value)
    {
        if (isset($this->options[$name])) {
            $this->options[$name] = $value;
        }
 
        return $this;
    }
 
    /**
     * Retorna uma configuração pelo nome
     *
     * @param string $name
     * @return mixed
     */
    public function getOption($name)
    {
        return isset($this->options[$name])
                ? $this->options[$name]
                : null;
    }
 
    /**
     * Define as classes de CSS para datas específicas
     *
     * @param array $cssClasses
     * @return My_View_Helper_Calendar
     */
    public function setCssClasses(array $cssClasses)
    {
        foreach($cssClasses as $cssClass => $dates) {
            $this->addCssClass($cssClass, $dates);
        }
        return $this;
    }
 
    /**
     * Adiciona uma classe de CSS para uma data específica
     *
     * @param string $cssClass
     * @param mixed $dates
     * @return My_View_Helper_Calendar
     */
    public function addCssClass($cssClass, $dates)
    {
        if (!in_array($cssClass, $this->protectedCssClasses)) {
            if (is_string($dates)) {
                $dates = array($dates);
            }
            if (is_array($dates)) {
                foreach($dates as $date) {
                    if (is_string($date) && Zend_Date::isDate(
                        $date, $this->getFormat('input')))
                    {
                        $this->cssClasses[$cssClass][] = $date;
                    }
                }
            }
        }
        return $this;
    }
 
    /**
     * Retorna a lista de classes de css definidas
     *
     * @return array
     */
    public function getCssClasses()
    {
        return $this->cssClasses;
    }
 
    /**
     * Retorna todas as classes de css, tanto as padrão quanto 
     * as definidas
     *
     * @return array
     */
    public function getAllCssClasses()
    {
        return array_merge($this->protectedCssClasses,
                           array_keys($this->getCssClasses()));
    }
 
    /**
     * Adiciona conteúdo html a uma data específica
     *
     * @param string $date
     * @param string $html
     * @return My_View_Helper_Calendar
     */
    public function insertHtml($date, $html=null)
    {
        if (is_array($date)) {
            foreach($date as $d => $h) {
                $this->insertHtml($d, $h);
            }
        } else if (Zend_Date::isDate($date, $this->getFormat('input'))) {
            if (is_string($html)) {
                $this->inserts[$date][] = $h;
            } else if (is_array($html)) {
                foreach($html as $h) {
                    if (is_string($h)) {
                        $this->inserts[$date][] = $h;
                    }
                }
            }
        }
 
        return $this;
    }
 
    /**
     * Retorna o conteúdo HTML adicionado para uma data específica
     *
     * @param string $date
     * @return array
     */
    public function getInserts($date=null)
    {
        if ($date) {
            if (isset($this->inserts[$date])) {
                return $this->inserts[$date];
            }
        } else {
            return $this->inserts;
        }
    }
 
    /**
     * Define a posição do conteúdo inserido nas datas
     * -1 = Antes da tag do link da data
     * 0 = Antes da data, dentro da tag do link
     * 1 = Depois da data, dentro da tag do link
     * 1 = Depois da tag do link da data
     *
     * @param int -1 = Before, 0 = Prepend, 1 = Append, 2 = After
     * @return My_View_Helper_Calendar
     */
    public function setInsertsPosition($position)
    {
        if (in_array($position, 
            array(self::INSERT_APPEND, self::INSERT_PREPEND))) 
        {
            $this->setOption('insertsPosition', $position);
        }
        return $this;
    }
 
 
    /**
     * Returna a posição em que o conteúdo será inserido
     *
     * @return int
     */
    public function getInsertsPosition()
    {
        return $this->getOption('insertsPosition');
    }
 
    /**
     * Define a data selecionada
     *
     * @param string $date
     * @return My_View_Helper_Calendar
     */
    public function setDate($date)
    {
        if ($date instanceof Zend_Date) {
            $this->date = $date;
        } else {
            $this->date = new Zend_Date($date, self::getFormat('input'),
                                        $this->locale);
        }
        return $this;
    }
 
    /**
     * Exibe um calendário HTML
     *
     * @param string $date Data selecionada
     * @param array $options
     * @return string
     */
    public function calendar($date=null, array $options=null)
    {
        if ($options) {
            $this->setOptions($options);
        }
 
        $this->now = Zend_Date::now($this->locale);
 
        if ($date) {
            $this->setDate($date);
        } else {
            $this->date = clone $this->now;
        }
 
        return $this->render();
    }
 
    /**
     * Gera o código HTML do calendário
     *
     * @return string
     */
    public function render()
    {
        // Cria um objeto Zend_Date para a data inicial
        $dateInit = clone $this->date;
        $dateInit->setDay(1); // Altera para o primeiro dia do mês
        $weekday = $dateInit->get(Zend_Date::WEEKDAY_DIGIT);
        $month = $this->date->get(Zend_Date::MONTH);
 
        // Calcula o primeiro dia da tabela a ser exibido
        $firstDay = clone $dateInit->subDay($weekday);
 
        // Cria uma div para o calendário com um id único
        $xhtml = '<div class="calendar" '
                . 'id="calendar_' . $this->date->get('yyyy-MM') . '">'
                . '<div class="calendar-header">';
 
        // Verifica se o calendário está configurado para exibir os 
        // links de navegação para exibir o mês anterior
        if ($this->getOption('showPrevMonthLink'))
        {
            $prevMonth = clone $this->date;
 
            // Cria um objeto para o mês anterior
            $prevMonth->subMonth(1); 
 
            $xhtml .= '<span class="prev-month-link">'
                  . '<a href="'
                  . $this->getOption('monthBaseUrl')
                  . $prevMonth->get('yyyy-MM').'" title="'
                  . $prevMonth->get(self::getFormat('header'))
                  .'">'
                  . $prevMonth->get(self::getFormat('prevAndNextMonth'))
                  . '</a></span>';
        }
 
        // Insere o nome do mês atual de acordo com o formato definido
        $xhtml .= '<span class="current-month">'
                . $this->date->get(self::getFormat('header'))
                . '</span>';
 
        // Verifica se o calendário está configurado para exibir os
        // links de navegação para exibir o mês seguinte
        if ($this->getOption('showNextMonthLink'))
        {
            $nextMonth = clone $this->date;
 
            // Cria um objeto para o mês seguinte
            $nextMonth->addMonth(1); 
 
            $xhtml .= '<span class="next-month-link">'
                  . '<a href="'
                  . $this->getOption('monthBaseUrl')
                  . $nextMonth->get('yyyy-MM').'" title="'
                  . $nextMonth->get(self::getFormat('header'))
                  .'">'
                  . $nextMonth->get(self::getFormat('prevAndNextMonth'))
                  . '</a></span>';
        }
 
        // Cria a tabela do calendário
        $xhtml .= '</div><table cellspacing="0" cellpadding="0" '
                . 'border="0"><thead>';
 
        $tmpDate = clone $firstDay;
 
        // Adiciona sete colunas com os nomes dos dias da semana
        for($i=0; $i<=6; $i++) {
            $cssClasses = array();
 
            // Define as classes de CSS da semana e da coluna
            if ($i == 0) {
                // Classe CSS para a primeira coluna
                $cssClasses[] = 'column-first';
            } else if ($i == 6) {
                // Classe CSS para a última coluna
                $cssClasses[] = 'column-last';
            }
 
            // Adiciona classes CSS diferentes para dias da semana
            // e fim-de-semana
            $cssClasses[] = (in_array(
                $tmpDate->get(Zend_Date::WEEKDAY_DIGIT), array(0, 6)))
                ? 'weekend-day' : 'week-day';
 
            // Adiciona uma classe específica para cada dia da semana
            $cssClasses[] = strtolower(
                $tmpDate->get(Zend_Date::WEEKDAY_NAME, 'en_US'));
 
            // Insere as classes CSS na célula e imprime o dia da semana
            // de acordo com o formato definido
            $cssClassesString = $cssClasses 
                ? ' class="'.implode(' ', $cssClasses).'"' 
                : '';
 
            $xhtml .= '<th' . $cssClassesString . '>'
                    . $tmpDate->get(self::getFormat('weekdays'))
                    . '</th>';
 
            // Avança para o dia seguinte e repete o processo
            $tmpDate->addDay(1);
        }
        $tmpDate = $firstDay;
        $xhtml .= '</thead><tbody>';
 
        // Cria sete linhas na tabela para as semanas
        for($i=0; $i<6; $i++) {
            $xhtml .= '<tr>';
 
            // Cria sete colunas para as datas
            for($j=0; $j<=6; $j++) {
                $day = $tmpDate->get(Zend_Date::DAY);
 
                $cssClasses = array();
 
                // Define as classes de CSS da semana e da coluna
                if ($j == 0) {
                    $cssClasses[] = 'column-first';
                } else if ($j == 6) {
                    $cssClasses[] = 'column-last';
                }
 
                // Adiciona classes CSS diferentes para dias da semana
                // e fim-de-semana
                $cssClasses[] = (in_array(
                    $tmpDate->get(Zend_Date::WEEKDAY_DIGIT),array(0,6)))
                    ? 'weekend-day' : 'week-day';
 
                //Adiciona uma classe específica para cada dia da semana
                $cssClasses[] = strtolower(
                        $tmpDate->get(Zend_Date::WEEKDAY_NAME, 'en_US'));
 
                // Adiciona classes CSS diferentes para hoje, dias 
                //passados e futuros
                switch($tmpDate->compareDate($this->now)) {
                    case -1: $cssClasses[] = 'past-day'; break;
                    case 0: $cssClasses[] = 'today'; break;
                    case 1: $cssClasses[] = 'future-day'; break;
                }
 
                // Adiciona uma classe CSS para a data selecionada
                if ($tmpDate->compareDate($this->date) == 0) {
                    $cssClasses[] = 'selected-date';
                }
 
                // Adiciona classes CSS para dias de outros meses
                if ($tmpDate->get(Zend_Date::MONTH) != $month) {
                    $cssClasses[] = 'other-month';
 
                    if (!$this->getOption('showOtherMonthDays')) {
                        $day = '&nbsp;';
                    }
 
                    // Adiciona classes CSS diferentes para dias de 
                    // meses passado e futuro
                    if ($tmpDate->get(Zend_Date::MONTH) < $month) {
                        $cssClasses[] = 'prev-month';
                    } else if ($tmpDate->get(Zend_Date::MONTH)>$month){
                        $cssClasses[] = 'next-month';
                    }
                }
 
                // Insere classes CSS definidos para datas específicas
                $addCssClasses = $this->getCssClasses();
                if ($addCssClasses) {
                    foreach($addCssClasses as $cssClass => $d) {
                        if ((is_string($d) && $tmpDate->get(
                          $this->getFormat('input')) == $d) ||
                          (is_array($d) && in_array(
                          $tmpDate->get($this->getFormat('input')),$d)))
                        {
                            $cssClasses[] = $cssClass;
                        }
                    }
                }
 
                // Insere conteúdo HTML definido para datas específicas
                $htmlInserts = '';
                $inserts = $this->getInserts(
                    $tmpDate->get($this->getFormat('input')));
                if ($inserts) {
                    foreach($inserts as $insert) {
                        $htmlInserts .= $insert . PHP_EOL;
                    }
                }
 
                // Creia a célula da tabela para a data
                $xhtml .= '<td '
                        . 'id="calendar_day_' 
                        . $tmpDate->get('yyyy-MM-dd') . '" '
                        . 'class="' . implode(' ', $cssClasses) . '">'
                        . ($this->getInsertsPosition() == 
                          self::INSERT_BEFORE ? $htmlInserts : '')
                        . '<a class="day" href="'
                        . $this->getOption('dateBaseUrl')
                        . '/'
                        . $tmpDate->get('yyyy-MM-dd')
                        .'">'
                        . ($this->getInsertsPosition() == 
                          self::INSERT_PREPEND ? $htmlInserts : '')
                        . $day
                        . ($this->getInsertsPosition() == 
                          self::INSERT_APPEND ? $htmlInserts : '')
                        . '</a>'
                        . ($this->getInsertsPosition() == 
                          self::INSERT_AFTER ? $htmlInserts : '')
                        . '</td>';
 
                $tmpDate->addDay(1);
            }
            $xhtml .= '</tr>';
        }
 
        $xhtml .= '</tbody></table></div>';
 
        return $xhtml;
    }
}

Bom, a classe é essa aí. Parece muito grande pra uma coisa aparentemente tão simples, mas acredito que o importante é a praticidade que ela proporciona para fazermos o calendário da forma que queremos. Então, vamos ver como utilizá-la:

<?php echo $this->calendar('2012-02-17', array(
    // Faz com que o link para o mês passado seja exibido
    'showPrevMonthLink'  => true,
    // Faz com que o link para o mês seguinte seja exibido
    'showNextMonthLink'  => true,
    // Faz com que os dias de outros meses sejam exibidos
    'showOtherMonthDays' => true,
    // Define a url base para os links das datas
    'dateBaseUrl'        => 'http://www.jaimeneto.com/data/',
    // Define a url base para os links de navegação dos meses
    'monthBaseUrl'       => 'http://www.jaimeneto.com/data/',
    // Define uma classe css "feriado" para as datas no array
    'cssClasses' => array(
        'feriado' => array(
            '2012-02-01', // Dia do Publicitário
            '2012-02-02', // Dia de Iemanjá
            '2012-02-22', // Carnaval
            '2012-02-16',  // Dia do Repórter
            '2012-03-08'  // Dia da Mulher
        )
    ),
    // Dá um destaque maior inserindo o nome do feriado do carnaval
    'inserts' => array(
        '2012-02-22' => array('<div>Carnaval</div>')
    ),
    // Define que o html deve ser inserido dentro do link, após a data
    'insertsPosition'    => My_View_Helper_Calendar::INSERT_APPEND
)) ?>

Vamos decorar o calendário agora com CSS, aproveitando todo o esforço para separar cada parte em classes agora.

body { font-family:arial;font-size:14px }
.calendar { width: 300px;cursor:default; }
.calendar a { text-decoration:none; color:#000; }
.calendar a:hover { text-decoration:underline; }
.calendar .calendar-header { border:1px solid #000;border-bottom:0;
    background-color:#ccc;text-align:center;text-transform:uppercase;
    line-height: 20px; }
.calendar .calendar-header .current-month { font-weight:bold; }
.calendar .calendar-header .prev-month-link { float:left;margin:0 3px; }
.calendar .calendar-header .next-month-link { float:right;margin:0 3px; }
.calendar table { border-collapse:collapse;width:100%; }
.calendar table td, .calendar table th { border:1px solid #000;
    text-align:center; line-height: 20px; }
.calendar table td a { display:block; margin:2px; 
    border: 1px solid transparent; }
.calendar table .weekend-day,.calendar table th {background-color:#eee;}
.calendar table td.other-month a { color:#ccc; }
.calendar table td.today { background-color:orange; }
.calendar table td.selected-date a { background-color:yellow; }
.calendar table td.feriado a { color:blue;border-color:blue; }
.calendar table td.other-month.feriado a {color:#ccc;border-color:#ccc;}

Vamos ver o resultado disso tudo:

Beleza, agora está pronto pra uso. Só pra finalizar, e pra mostrar que dá pra fazer uma coisa realmente elegante com isso, apenas mexendo no CSS, e, neste caso, usando o plugin qTip do Jquery, vou mostrar aqui o resultado do propósito final pra eu trabalhar tanto nessa classe. Ela foi criada para o site de eventos letz.com.br, e queríamos que as datas com eventos marcadas pelo usuário ficassem destacadas no calendário, e que quando ele passasse o cursor sobre a data, aparecece os eventos num balão. Veja como ficou:

Pronto. É isso, galera. Espero que tenham gostado e que seja útil pra alguém! ~__^

·

8 comments

  • Igor Herson · 18 de abril de 2012 às 2:34

    Antes de mais nada agradeço bastante pela classe, porem estou com dificuldades na implementacao, ela esta apresentando alguns comportamentos estranhos, primeiro de tudo em minha maquina os dias estão saltando e pulando sempre uma data (tipo 2, 4, 6, etc) segundo (provavelmente pelo mesmo motivo do problema anterior) o destaque nas datas de sabado estão indo para a coluna da quarta feira, e terceiro, tento mudar os parametros do css apresentado acima, mas nao modifica em nada no codigo.

    Responder

    • Admin comment by jaime · 19 de abril de 2012 às 15:25

      Igor, realmente tinha uns errinhos lá no script.
      Faltavam alguns CSS e no final tinha uma linha repetida que incrementa um dia. Ou seja, estava incrementando dois dias em vez de um só: $tmpDate->addDay(1);

      Corrigido! Obrigado pelo aviso.

      Responder

  • Soares · 19 de maio de 2012 às 10:42

    Bom Colega…
    Poderia esplicar para nos como implatar sua class.

    applicatin/views/helpers/Calendar.php
    a segunda parte do script coloquei na Views mais naos mostra nada, nem erros, tudo em branco.

    poderia me da uma dica, estou aprendo ainda no zend

    Responder

  • André · 22 de outubro de 2013 às 14:56

    Boa tarde,

    Encontrei um problema ao usar o seu helper, acredito que nem todos tenham, mas quando for usar, é melhor cuidar com o horário de verão, e setar um time zone que não horário de verão.

    Solucionei o problema usando:
    $this->date->setTimezone(‘UTC’);

    Att,

    Responder

  • Jean Gomes · 26 de junho de 2014 às 15:22

    Olá, sei que tem muito tempo o artigo, já temos o zend 2, mas estou utilizando seu helper em um projeto atualmente com zend 1, e está dando problema em outubro, não importa o ano, ele duplica um dia do mês, um sábado. Não consegui achar a causa do problema, agradeço se puder ajudar.

    Responder

    • Admin comment by jaime · 27 de junho de 2014 às 7:35

      Beleza, Jean. Assim que tiver um tempinho vou dar uma olhada.

      Responder

  • Jean Gomes · 7 de agosto de 2014 às 11:57

    Ok.

    Responder

    • Admin comment by jaime · 27 de outubro de 2014 às 0:14

      Olá, Jean. Sei que faz tempo que falei que daria uma olhada no código do calendário, mas finalmente o fiz. Disponibilizei o código com os ajustes no github no projeto ZFUtil, onde colocarei o que for fazendo de útil para ZF1. Depois devo fazer para o ZF2 também, mas ainda não estou usando muito ele… Eis o link: https://github.com/jaimeneto/ZFUtil

      Responder

Leave a Reply

<<

>>

Theme Design by devolux.nh2.me