MODx — собственный ajax календарь событий/новостей без Ditto

Задали мне тут намедни задачку: сделать календарь событий на ModX Evolution.
Движок этот я искренне люблю, за его небывалую гибкость и лично мне понятность.

Задача показалась простой, но по мере выполнения встретился с парой сложных моментов.
Итак, кто хочет сделать у себя на сайте календарик с всплывающим списком событий — прошу под хаброкат!



Для выполнения задачи нам понадобятся:
  • Документ (контейнер) с новостями. Это должны быть обычные документы modx.
  • Javaскрипт библиотеки jQueryUI и qTip. Первая будет рисовать календарь и обеспечивать его работу, а вторая — всплывающие события.
  • Самописный сниппет eventsCalendar, который мы скоро вместе напишем.
  • 2 чанка (html оформления). Первый общий, где будет вызов скриптов и оформление календаря, и второй — оформление одного события во всплывающем окне.
  • Параметр TV для документов-событий. Создается в админке modx.


Для начала, создадим TV параметр для документов-событий. Идем в админке modx в Элементы->Управление элементами->Параметры (TV)->Новый параметр (TV)
Имя параметра: event_date
Заголовок: event_date
Тип ввода: Date
Шаблоны: любой, главное, чтобы он был выставлен шаблоном у событий.

Теперь, когда вы зайдете в событие, внизу появится этот параметр, при клике будет окно выбора даты, по которой мы и выведем это событие в календаре.

Далее, нам нужно написать сниппет, который будет выдавать массив событий для jQueryUI.datepicker. Вообще, этот плагин предназначен не для
вывода событий, а для выбора дат. Возможно, использовать его для другой функции не совсем верное решение, но есть несколько причин, по которым я его использую так:

Во-первых, jQueryUI у меня уже есть на сайте, так зачем подключать что-то еще?
Во-вторых, datepicker отлично настраивается, есть возможность указать диапазон выводимых дат, повешать событие на выбор даты, на смену месяца и тд.
Все это позволит создать симпатичный календарь событий с ajax запросами без изобретения собственного велосипеда.
Примерно так я думал.

На деле, оказалось, что этот подход несет в себе пару неудобств, но о них позже.

Сниппет eventsCalendar. Работа на стороне сервера.

Итак, пишем сниппет. С некоторых пор, я уже «hello world!» не могу вывести без написания класса, так что, заранее извините.

<?php
class eventsCalendar {

    var $id; // Номер документа-контейнера событий
    var $dateFormat = '%d %b %Y %H:%M'; // Формат даты, используется strftime()
    var $dateTV = 'event_date'; // название TV параметра modx для документа
    var $tplEvent = 'tplEvent'; // Шаблон оформления каждого события
    var $tplMain = 'tplCalendar'; // Общий шаблон оформления календаря событий
    var $conv = 0; // Нужна ли конвертация событий из cp1251 в utf8. По умолчанию - не нужна. 

/*	Вывод ошибок	*/
    function error($err) {
        $arr = array(
                     'no_id' => 'Вы забыли указать id каталога для выборки событий',
                     'no_action' => 'Не указан метод для обработки ajax запроса.',
                     'no_result' => 'В заданом контейнере нет документов.'
                    );
        return $arr[$err];
    }
    
/*	Вывод событий, принимает номер контейнера событий, месяц и год	*/
    function getEvents($id = '', $month = '', $year = '') {
        global $modx;

        if (empty($id)) {return $this->error('no_id');}
        if (empty($month)) {$month = date('m');}
        if (empty($year)) {$year = date('Y');}
        if (strlen($month) == 1) {$month = '0'.$month;}
        
        /*	Получаем документы из контейнера, нас интересует их id и дата	*/
        $tmp = $modx->getDocumentChildrenTVars($id, array('id',$this->dateTV));
        
        /*	Собираем массив в нужном формате для дальнейшей обработки	*/
        if (empty($tmp)) {return $this->error('no_result');}
        else {
            foreach ($tmp as $v) {
                $d = strftime('%Y-%m', strtotime($v[0]['value']));
                if ($d == $year.'-'.$month) {$ids[] = $v[1]['value'];}

                $dates_arr[$v[1]['value']] = $v[0]['value'];
            }         
        }

		/*	Мы получили массив нужных документов $ids, и массив дат этих документов, друг с другом они не связаны, пока
			Теперь уже достаем документы полностью. Сортировка по дате побликации		*/
        $arr = $modx->getDocuments($ids, 1, 0, "id,pagetitle,introtext", "", 'pub_date ASC, id', 'ASC');

		/*	Если подходящих документов не найдено - возвращаем закодированый пустой массив, чтобы
			вывести пустой календарь	*/
        if (empty($arr)) {return json_encode(array());}

		/*	Если документы есть - обрабатываем их свойства, оформление и запихиваем в массив	*/
        /*	Для начала достаем указаный шаблон оформления каждого события	*/
        $tpl = $modx->getChunk($this->tplEvent);
        
        $i = 1;
        foreach ($arr as $v) {
        	/*	определяем переменные документа для подстановки в шаблон	*/
            $did = $v['id'];
            $url = $modx->makeUrl($did);
            $date = strftime($this->dateFormat, strtotime($dates_arr[$did]));
            $date2 = strftime('%Y-%m-%d', strtotime($dates_arr[$did]));
            $desc = $v['introtext'];
            $title = $v['pagetitle'];    
            /*	Это для номера события, если событие не одно в дне	*/
            if (isset($date3) && $date3 != $date2) {$i = 1;}
            $date3 = $date2;
            
            /*	Стандартная замена плейсхолдеров на переменные в шаблонах modx	*/
            $placeholders = array('[+ec.date+]','[+ec.title+]','[+ec.url+]','[+ec.desc+]','[+ec.num+]');
            $values = array($date, $title, $url, $desc, $i);
            $text = str_replace($placeholders, $values, $tpl);
            
            /*	Забивание результата в массив, с возможной перекодировкой из cp1251.	*/
            if ($this->conv != 0) {$dates[$date2] .= iconv('cp1251', 'utf-8', $text);}
            else {$dates[$date2] .= $text;}
            
            $i++;
        }

        /*	Т.к. jqueryui.datepicker нужен массив 'дата','css class','текст события' - приводим наш результат к такому виду	*/
        foreach($dates as $k => $v) {
            $dates2[] = array($k, '', $v);
        }
		/*	Возвращаем данные в формате json	*/
       return json_encode($dates2); 
    }

	/*	Просто вывод основного шаблона на экран, по умолчанию	*/    
    function output($tpl) {
        global $modx;
        
        $tpl = $modx->getChunk($tpl);
        
        echo $tpl;
    }

}
/*	Конец класса, дальше обвязка, для его использования в modx	*/

$Cal = new eventsCalendar;

$Cal->id = $id;	// Указывать обязательно!
/*	Если заданы параметры при вызове сниппета - переопределяем стандартные	*/
if (!empty($dateTV)) {$Cal->dateTV = $dateTV;}
if (!empty($dateFormat)) {$Cal->dateFormat = $dateFormat;}
if (!empty($tplEvent)) {$Cal->tplEvent = $tplEvent;}
if (!empty($tplMain)) {$Cal->tplMain = $tplMain;}
if (!empty($conv)) {$Cal->conv = $conv;}

/*	Если идет запрос через ajax	- вызываем нужный метод и останавливаем работу	*/
if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
        $action = $_POST['action'];
        if (!empty($action)) {
            switch($action) {
                case 'getEvents': echo $Cal->getEvents($Cal->id, $_POST['month'], $_POST['year']); break;
            }
        }
        else {
            echo $Cal->error('no_action');
        }
    die();
}
/*	Если обычный вызов - выводим календарь на экран	*/
else {
    $Cal->output($Cal->tplMain);
}
?>


Далее, идем в админке modx в сниппеты, создаем новый, обзываем его eventsCalendar и копируем внутрь вышенаписанное.
Мы создали новый сниппет и вызываем его в любом месте страницы примерно так:
[!eventsCalendar?
&id=`13`
&dateTV=`event_date`
&dateFormat=`%d %b %Y %H:%M`
&tplMain=`tplCalendar`
&tplEvent=`tplEvent`
!]

Параметры смотрите в конце топика.

Только пока он ничего не выведет, ибо мы не написали шаблоны.


Чанки. Работа на стороне клиента.

Начнем с мелкого — шаблон одного события tplEvent.
<div class='event'>
    <span class='num'><b>[+ec.num+].</b></span>
    <span class='date'>[+ec.date+]</span>
    <span class='link'><a href='[+ec.url+]' target='_blank'>[+ec.title+]</a></span>
    <br />
    <span class='notice'>[+ec.desc+]</span>
</div>
<br />


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

Нужно подключить библиотеки (следите за путями).
<script type='text/javascript' src='[(site_url)]inc/js/jquery-1.4.4.min.js'></script> 
<script type='text/javascript' src='[(site_url)]inc/js/jquery-ui-1.8.6.custom.min.js'></script>
<script type='text/javascript' src='[(site_url)]inc/js/jquery.ui.datepicker-ru.js'></script> <!-- Локализация datepicker -->
<script type='text/javascript' src='[(site_url)]inc/js/jquery.qtip.js'></script>


Теперь, основная магия.
<script type='text/javascript'>
$(document).ready(function() {
    class_enabled = 'enabled'; // Класс оформления даты, в которой есть события
    class_disabled = 'disabled'; // Нет событий
    element = '#Calendar'; // Элемент DOM в который грузится календарь
    url = '/[~[*id*]~]'; // Урл страницы для отправки ajax, по умолчнию - текущая

    dates = getEvents(); // Достаем массив событий через ajax
    
    Calendar(dates); // Запускаем календарь и отдаем ему массив с датами
    Qtip();	// Запускаем qTip, для всплывающих событий
});

// Функция вызова календаря на экран. 
function Calendar(dates) {
    $(element).datepicker({
        language: 'ru',
        inline: true,
        dateFormat: 'dd.mm.yy',
        // При смене месяца - получаем новый массив с датами. После этого, через 1 секунду запускаем qTip, 
        // чтобы он сделал вновь созданные элементы DOM всплывашками.
        onChangeMonthYear: function(year, month) {
            dates = getEvents(year, month);
            window.setTimeout(
                function() {
                    Qtip()
                },
                1000
            );
        },
        // Перед показом каждой даты - прогоняем ее по массиву событий, чтобы выставить свойства.
        // Свойств 3: вкл/выкл, класс оформления и текст, который вставляется в title элемента td.
        beforeShowDay: function(d) {
            var date = $.datepicker.formatDate('yy-mm-dd', d);
            for (i = 0, c = dates.length; i < c; i++) {
                if (date == dates[i][0]) {
                    return [true, class_enabled, dates[i][2]];
                }
            }
            return [false, class_disabled];
       },
       // Что делать при клике по дате. У меня - ничего не делать.
       onSelect: function() {return false;}
    });
    
}

// Запрос массива событий через ajax из нашего сниппета.
// Тут важный момент - СИНХРОННОСТЬ запроса, потому, что, пока не придут данные - календарь нельзя отрисовывать.
// Иначе он все время будет пуст, так как данные придут позже, чем он выведется на экран.
function getEvents(year, month) {
    $.ajaxSetup({async: false});
    $.post(url, {action: 'getEvents', month: month, year: year}, 
        function(data) {
            if (data == 'null') {data = '[]';}
            dates = jQuery.parseJSON(data);
            response = dates; 
        }        
    )
    return response;            
}

// Запуск всплыващих подсказок qTip.
// Все параметры можно поглядеть на сайте автора. Выводится текст из title элемента.
function Qtip() {
    $(element + ' .' + class_enabled).qtip({
        prerender: true,
        show: {when: {event: 'mouseover'}, effect: {length: 0}, solo: true},
        hide: {when: {event: 'unfocus'}, effect: {length: 0}},
        position: {
            corner: {
                target: 'center',
                tooltip: 'topLeft'
            }
        },
        style: {height: 100,width: 300}
    });
}
</script>

<div id='Calendar'></div> -- Элемент, куда будет грузиться календарь

У меня еще используются некоторые штучки, чтобы затемнять календарь при смене месяца и выводить индикатор загрузки.
Ничего сложного, посмотреть можно тут.

Как это работает?

При первом открытии страницы нам выводится просто шаблон tplCalendar. После его загрузки стартует функция запроса массива дат.
Как только, придет массив — запускается календарь, который при открисовке даты назначает ей стиль и текст для всплывающей подсказки.
Затем запускается qTip и генерирует всплывающие подсказки.

При смене месяца — запрашивается новый массив дат и заново запускаются всплывашки (так как DOM изменился).

При этом, мы можем понавешать события на клик по дате (например, переход на страницу с новостями по этой дате). На смену месяца (у меня
вылезает индикатор загрузки + затемняется сам календарь). Вид сплывающего окна полностью настраивается, как стиль, так и содержимое.

Теперь о недостатках. Их несколько.
  • Необходимость вызова qTip после каждой операции с календарем. Так как невозможно узнать (по крайней мере, я не придумал как), что календарь
    готов, приходится делать это по таймауту в 1сек. Если сервер тормозной, или событий ОЧЕНЬ много, секунды может не хватить. С другой стороны,
    таймаут можно поменять, да и событий создать столько, чтобы обычный сервер не успел их обработать за секунду, я не смог.
  • Так как текст для всплывающего окна пишется в атрибут title элемента td, приходится помнить о ковычках и их экранировании.

Недостатки можно было бы обойти написанием своего календаря, на php под это конкретную задачу, но я решил, что достоинства datepicker перевешивают недостатки его использования для вывода событий.

Приложение.

Скачать архив со сниппетом и шаблонами
Посмотреть в работе (версия для Revolution)

Параметры сниппета eventsCalendar:
&id
по умолчанию: нет, но обязателен
значение: [int]
описание: ID существующего документа-контейнера событий.

&dateFormat
по умолчанию: '%d %b %Y %H:%M'
значение: переменные strftime()
описание: Формат даты.

&dateTV
по умолчанию: event_date
значение: Имя существующего tv параметра
описание: название TV параметра modx для документа.

&tplEvent
по умолчанию: tplEvent
значение: Имя существующего чанка modx
описание: Шаблон оформления каждого события.

&tplMain
по умолчанию: tplCalendar
значение:
описание: Общий шаблон оформления календаря событий.

&conv
по умолчанию: 0
значение: [int]
описание: Нужна ли конвертация событий из cp1251 в utf8.


Плэйсхолдеры сниппета eventsCalendar:
[+ec.num+]
Номер события в дне. Актуально, только если у вас больше одного события в день. Для правильной нумерации необходимо,
чтобы параметр даты публикации документа совпадал с TV параметром даты события. Иначе, нумерация может быть неверной.

[+ec.date+]
Дата события.
[+ec.url+]
Ссылка на событие.
[+ec.title+]
Заголовок, берется из pagetitle документа modx.
[+ec.desc+]
Краткое описание, берется из introtext документа modx.


Ссылки на нужные сайты.


UPD.
Багрепорты засылать на bezumkin@yandex.ru, или оставлять в этой теме.

UPD.2
Готова вторая версия календаря, БЕЗ datepicker, и связанных с ним недостатков!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 26

    0
    Спасибо, хороший календарь, а вам плюс. Хочу опробовать его в действии в следующем проекте, позже отпишусь, если замечу какие-либо баги.
      +1
      На здоровье! Сейчас думаю сделать версию №2, с обычным календарем, а не datepicker.

      Пока праздники — все равно заняться больше нечем.
        0
        Может попробуете силы в написании простенькой системы управления проектами и тайм-менеджментом? Есть такие модуль в Друпал, правда русских нет. Хотим открыть второй сайт студии на МОДх, если такое сделаете, то пожизненный от нас респект.
          0
          У меня не настолько много свободного времени )

          Если кто-то что-то такое начнет делать и встретит мощный затык — готов помочь. Но самостоятельно поднимать какие-то проекты я пока не могу.

          Зато я могу постить потихоньку свои наработки на modx. Сегодня вот еще топик написал.
        0
        Понравился, спасибо.
          0
          весьма не плохо по самой реализации, нужно адаптировать под свой двиг с которым я постоянно работаю
            0
            Благодарствую, на днях буду тестировать.
            Багрепорт куда отсылать если что?
              0
              Пока можно на bezumkin@yandex.ru, или сюда.
              0
              Спасибо, очень интересное решение. А для чего параметр &conv был введен?
                0
                Это для тех бедных людей, у которых данные хранятся в БД в кодировке cp1251.

                При возврате массива данных используется json_encode(), которая работает только с utf8. Поэтому, весь русский текст нужно перекодировать cp1251 -> utf8.
                  0
                  Понятно, беглым взглядом я не заметил, что используется json_encode(). Хотя, в принципе, сейчас уже не так часто встречаются желающие использовать cp1251, так как с ним вообще половина расширений в MODx не работает без напильника.

                  Кстати, можно все это дело закопировать на community.modx-cms.ru, так это решение тоже многим пригодится, да и дополнительная уверенность, что архив однажды не пропадет никуда.
                    0
                    Я таки думаю еще сделать версию со стандартным календарем, без datepicker и поглядеть.

                    Если это решит все текущие проблемы — обновлю статью тут и, возможно, закопипастю туда.

                    Не нравится мне этот обязательный таймаут и то, что текст события можно только в title хранить. Лучше бы скрытый div.
                      0
                      Буду следить за развитием :)
                0
                сделайте интернационализацию и залейте на гуглокод/гитхаб чтоли, а то верно спрашивают куда баги засылать…
                  0
                  Я так понимаю, что интернационализация уже сделана jqueryui.datepicker. У меня в примере для календаря подключен только русский язык.

                  А про заливку на гугл сложнее — никогда этого не делал. Где можно почитать?
                    +1
                    про интернационализацию
                    тут надо бы сделать
                    $arr = array(
                    'no_id' => 'Вы забыли указать id каталога для выборки событий',
                    'no_action' => 'Не указан метод для обработки ajax запроса.',
                    'no_result' => 'В заданом контейнере нет документов.'
                    );
                    просто заведи переменную, по которой будешь палить язык интерфейса

                    что выбрать — гитхаб/гуглокод
                    зависит от программы контроля версий
                    github.com = git
                    code.google.com = svm,mercurial
                    brokenbrake.biz/2010/11/27/Git_vs_Mercurial gitvsmercurial.com/ www.wikivs.com/wiki/Git_vs_Mercurial

                    гитхаб хелпер
                    help.github.com/

                    гуглокод хелпер
                    code.google.com/p/support/wiki/GettingStarted

                    будут затруднения — тыкни в личку
                      0
                      Спасибо за разъяснения!

                      На мой взгляд, вышеприведененный топик слишком прост, чтобы выходить на мировой уровень, мутить контроль версий и все прочее. Это ж надо оформлять, следить, отвечать и т.д.

                      Давайте, я лучше буду следить за комментариями в этой теме и оперативно на них отвечать? С исправлениями, в случае нахождения багов, конечно?
                0
                >>Необходимость вызова qTip после каждой операции с календарем
                сделайте нормально, а если не придет ответ в течении 1 секунды? прокиньте колбек в вашу функцию getEvents, а внутри функции его запускайте, если таковой параметр присутсвует по success response.
                  0
                  Фикус в том, что getEvents() возвращает массив данных, который обрабатывается календарем при выводе каждой даты.

                  Сколько календарь будет обрабатывать это дело перед отрисовкой — не известно. Callback у datepicker beforeShowDay(), потому что она вызывается при каждой дате.

                  Единственное, что пока приходит на ум: посчитать, при вызове этой функции, не последний ли это день в месяце (дата передается), и если да — сделать вызов Qtip().

                  Позже попробую так.
                    0
                    Не выходит. Без таймаута с эти календарем не обойтись.

                    Даже если вычислять последний день в текущем месяце и сравнивать с ним при обработке каждой даты, все равно вызов Qtip() перед return ничего не даст, так как Qtip() будет искать инфу для всплывашек в еще не обновленном календаре.

                    Так что, либо мириться с этим недостатком, либо писать другой календарь.
                    Возможно, авторы datepicker добавят функционал.
                  0
                  В репозиторий MODx надеюсь выложите?
                    0
                    Сейчас, как и обещал, занимаюсь версией календаря на php, без datepicker, с решением всех текущих недостатков.

                    Как сделаю — обязательно выложу, если расскажите, как.
                  0
                  Внимание!
                  Готова вторая версия календаря, теперь БЕЗ datepicker.

                  Календарь отрисовывается на php.
                  Функциональность прежняя, глюков почти нет (qTip иногда тупит, пока не ясно от чего), возможностей настроек больше. Вообще, в целом, круче и удобнее. Пока можно поглядеть/потестировать, в ближайшее время причешу код и опубликую новый топик, с исходниками кодами.

                  Замеченые баги — постить сюда.

                  Only users with full accounts can post comments. Log in, please.