[EVO] SimpleGallery: расширение функционала, часть 2

В этой статье речь пойдет о том, как изменить интерфейс SimpleGallery под конкретную задачу. При этом файлы плагина останутся без изменений — а значит, работа не пропадет после обновления SimpleGallery. Дальше много текста и говнокода.

Задача была такая: cайт для производителя мебели, состоящий, по сути, из галерей с фотками мебели, которые были реализованы с помощью SimpleGallery. Когда сайт уже был готов, заказчик внезапно пожелал, чтобы с фотографией выводились:
  • цена;
  • размеры;
  • материал.

Кроме этого, для некоторых фотографий нужно показывать значок «Наша работа». Ну и пусть поле «Описание» редактируется с помощью TinyMCE (: А чтобы было красиво, основные поля и дополнительные поля будут редактироваться в отдельных вкладках.

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

Так как выборки и сортировки по новым полям не нужны, было решено не создавать дополнительные поля в таблице, а воспользоваться полем sg_properties, в котором хранятся параметры картинки в формате json. А для флажка «Наша работа» пригодится существующее поле sg_add.

Расширение классов

Так как было решено не добавлять дополнительные поля в таблицу, то достаточно внести изменения в контроллер. А именно изменить метод edit. Для этого создадим файл custom.class.php в папке assets/plugins/simplegallery/lib/:


<?php namespace SimpleGallery;

require_once (MODX_BASE_PATH.'assets/plugins/simplegallery/lib/controller.class.php');
//расширяем класс sgController
class customController extends sgController{
    public function edit(){
	$out = array();
	$id = isset($_REQUEST['sg_id']) ? (int)$_REQUEST['sg_id'] : 0;
        if ($id) {
	    $fields = array(
	        'sg_title' => $_REQUEST['sg_title'],
        	'sg_description' => $_REQUEST['sg_description'],
	        'sg_add' => $_REQUEST['sg_add']
	    );
	    $fields['sg_isactive'] = isset($_REQUEST['sg_isactive']) ? 1 : 0;
            $row = $this->data->edit($id);
            $_data = $row->toArray();
            //получили значения дополнительных полей
            $addProperties = array(
	        'price' 	=> $_REQUEST['price'],
                'msize' 	=> $_REQUEST['msize'],
	        'material' => $_REQUEST['material']
            );
            //добавили их в поле sg_properties
            $_data['sg_properties'] = array_merge($_data['sg_properties'],$addProperties);
            //отправили все поля на сохранение
            $_data = array_merge($_data,$fields);
            $out['success'] = $row->fromArray($_data)->save();
	} else {
	    $out['success'] = false;
	}
	return $out;
    }
}


Теперь в настройки плагина SimpleGallery вписываем имя нашего класса (SimpleGallery\customController) и создаем еще один плагин на событие OnManagerPageInit, который этот класс загрузит:


$e = $modx->event;
if ($e->name == "OnManagerPageInit") {
    if (isset($invokedBy) && $invokedBy=="SimpleGallery") include_once(MODX_BASE_PATH.'assets/plugins/simplegallery/lib/custom.class.php');
}


На этом с серверной частью все.

Изменение интерфейса

Здесь возникает две подзадачи. Во-первых, нужно изменить форму редактирования, во-вторых, внести изменения в обработку этой формы.

Если посмотреть папку assets/plugins/simplegallery/js/tpl/, то можно обнаружить там файлы с расширением *.handlebars. Это шаблоны для js-шаблонизатора Handlebars, в которые вынесены формы SimpleGallery + шаблон для превьюшки.

Синтаксис Handlebars достаточно простой и работа с такими шаблонами не сильно отличается от работы с чанками. Однако простое изменение файла editForm.handlebars ничего не даст — шаблоны нужно компилировать. Этот аспект я рассматривать не буду, скажу лишь, что потребуется установить где-нибудь nodejs и прочитать небольшую инструкцию в файле readme.txt, который можно найти в папке с шаблонами.

Создадим файл editFormMebel.handlebars:

<div id="editTabs" class="easyui-tabs">
    <!-- Первая вкладка -->
    <div title="Основные свойства">
        <div id="sgEdit">
            <div class="sgRow">
                <div style="font-size:0;text-align:center;">
                    <img src="{{modxSiteUrl}}{{data.sg_image}}">
                </div>
                <div>
                    <table>
                        <tr>
                            <td class="rowTitle">ID</td>
                            <td>{{data.sg_id}}</td>
                        </tr>
                        <tr>
                            <td class="rowTitle">{{sgLang.file}}</td>
                            <td>{{data.sg_image}}</td>
                        </tr>
                        <tr>
                            <td class="rowTitle">{{sgLang.size}}</td>
                            <td>{{data.sg_properties.width}}x{{data.sg_properties.height}}, {{bytesToSize data.sg_properties.size}}</td>
                        </tr>
                        <tr>
                            <td class="rowTitle">{{sgLang.createdon}}</td><td>{{data.sg_createdon}}</td>
                        </tr>
                    </table>
                </div>
            </div>
            <div class="sgRow">
                <div>
                    <form id="sgForm" class="sgForm"><input type="hidden" name="sg_id" value="{{data.sg_id}}">
                        <label>{{sgLang.title}}</label>
                        <input name="sg_title" maxlength="255" type="text" value="{{data.sg_title}}">
                        <label>{{sgLang.description}}</label>
                        <textarea name="sg_description">{{data.sg_description}}</textarea>
                        <a href="javascript:" class="btn-rte">Редактировать</a>
                        <label>{{sgLang.show}}</label>
                        <input type="checkbox" name="sg_isactive" value="1"{{#ifCond data.sg_isactive "1"}} checked{{/ifCond}}>{{sgLang.yes}}
                    </form>
                </div>
            </div>
            <div style="clear:both;padding:10px;float:right;">
                <div class="sgEditSave btn btn-right">
                    <div class="btn-text">
                        <img src="{{modxTheme}}/images/icons/save.png">
                        {{sgLang.save}}
                    </div>
                </div>
                <div class="sgEditCancel btn btn-right">
                    <div class="btn-text">
                        <img src="{{sgLang.save}}/images/icons/stop.png">
                        {{sgLang.cancel}}
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- Вторая вкладка -->
    <div title="Дополнительно" style="padding:10px;display:none;">
        <form id="sgAdd" class="sgForm">
            <label>Наша работа</label>
            <input type="checkbox" name="sg_add" value="1"{{#ifCond data.sg_isactive "1"}} checked{{/ifCond}}>{{sgLang.yes}}
            <label>Цена</label>
            <input type="text" name="price" value="{{data.sg_properties.price}}">
            <label>Размеры</label>
            <input type="text" name="msize" value="{{data.sg_properties.msize}}">
            <label>Материалы</label>
            <input type="text" name="material" value="{{data.sg_properties.material}}">
        </form>
        <div style="clear:both;padding:10px;float:right;">
                <div class="sgEditSave btn btn-right">
                    <div class="btn-text">
                        <img src="{{modxTheme}}/images/icons/save.png">
                        {{sgLang.save}}
                    </div>
                </div>
                <div class="sgEditCancel btn btn-right">
                    <div class="btn-text">
                        <img src="{{modxTheme}}/images/icons/stop.png">
                        {{sgLang.cancel}}
                    </div>
                </div>
            </div>
    </div>
</div>


Разметка, конечно, не очень, но не важно. Компилируем:

handlebars editFormMebel.handlebars -f editFormMebel.js -m


Эта команда скомпилирует шаблон в файл editFormMebel.js и минифицирует его.

Теперь создадим еще одну форму, для редактирования описания с помощью TinyMCE. Эта форма будет показываться в окошке, если кликнуть по ссылке «Редактировать». Файл rteForm.handlebars:

<div id="rteForm">
    <div style="width:600px;">
        <textarea id="rteField" style="width:99%;height:400px;">{{textarea}}</textarea>
    </div>
    <div style="clear:both;padding:10px;float:right;">
        <div id="rteSave" class="btn btn-right">
            <div class="btn-text">
                <img src="{{modxTheme}}/images/icons/save.png">
                {{sgLang.save}}
            </div>
        </div>
        <div id="rteCancel" class="btn btn-right">
            <div class="btn-text">
                <img src="{{modxTheme}}/images/icons/stop.png">
                {{sgLang.cancel}}
            </div>
        </div>
    </div>
</div>

Компилируем:

handlebars rteForm.handlebars -f rteForm.js -m


Файлы editFormMebel.js и rteForm.js копируем в assets/plugins/simplegallery/js/tpl/

Теперь добавим методы для вызова окошка с формой редактирования описания, для инициализации TinyMCE, а также изменим метод sgHelper.edit(). Создаем в папке assets/plugins/simplegallery/js/plugin/ файл editForm.js:

(function($){
    sgHelper.rteForm = function(textarea) {
        //готовим данные для передачи в шаблон
        var context = {
            textarea: textarea.val(),
            modxTheme: sgConfig._modxTheme,
            modxSiteUrl: sgConfig._modxSiteUrl,
            sgLang: _sgLang
        };
        //получаем заполненный шаблон
        var rteForm = $(Handlebars.templates.rteForm(context));
        //создаем окошко
        rteForm.window({
            modal:true,
            title:"Редактирование",
            collapsible:false,
            minimizable:false,
            maximizable:false,
            resizable:false,
            onOpen: function() {
                //когда окошко создано, нужно назначить обработчики для кнопок в форме
                $('#rteCancel').click(function(e){
                    rteForm.window('close',true);
                });
                $('#rteSave').click(function(e){
                    //берем то, что наредактировали и сохраняем обратно в поле "Описание"
                    var content = tinyMCE.activeEditor.getContent();
                    textarea.val(content);
                    rteForm.window('close',true);
                });
                //цепляем редактор к полю в форме
                sgHelper.initRTE();
            },
            onClose: function() {
                //если окошко закрывается, то удаляем его вообще
                //после закрытия окна с редактором нужно восстановить оверлей
                var mask = $('.window-mask');
                sgHelper.destroyWindow(rteForm);
                $('body').append(mask);
            }
        })
    };
	sgHelper.edit = function(image) {
	    var data = image.data('properties');
            var context = {
                data: data,
                modxTheme: sgConfig._modxTheme,
                modxSiteUrl: sgConfig._modxSiteUrl,
                sgLang: _sgLang
            };
            //получаем форму редактирования
	    var editForm = $(Handlebars.templates.editFormMebel(context));
		editForm.window({
    		modal:true,
    		title:sgHelper.escape(this.stripText(data.sg_title,80)),
    		doSize:true,
    		collapsible:false,
    		minimizable:false,
    		maximizable:false,
    		resizable:false,
    		onOpen: function() {
    	            //создаем вкладки
                    $('#editTabs').tabs({
                        border:false,
                        onSelect:function(title,index){
                            if (index) {
                            var tab = $('#editTabs').tabs('getTab',index);
                            $(tab).show();
                            editForm.window('resize');
                        }
                        }
                    });
                    //обработчики кнопок
                    //показываем окошко с редактором
                    $('.btn-rte').click(function(e){
                        sgHelper.rteForm($('textarea','#sgEdit'));
                    });
                    $('.sgEditCancel').click(function(e){
    			editForm.window('close',true);
    		    });
    		    $('.sgEditSave').click(function(e){
    			$.post(
		            sgConfig._xtAjaxUrl+'?mode=edit', 
			    $('#sgForm,#sgAdd').serialize(),
			    function(data) {
                                data = sgHelper.getData(data);
				if(data.success) {
				    editForm.window('close',true);
				    sgHelper.update();
				} else {
				    $.messager.alert(_sgLang['error'],_sgLang['save_fail']);
				}
			})
    		    })
    		},
    		onClose: function() {
                    sgHelper.destroyWindow(editForm);
    			}
    	    });
	}
})(jQuery);


Инициализацию TinyMCE я вынес в отдельный файл, assets/plugins/simplegallery/js/plugin/tinymce.js:

(function($){
sgHelper.initRTE = function() {
tinyMCE.init({
    theme                            : 'advanced',
    skin                             : 'cirkuit',
    skin_variant                     : '',
    mode                             : 'exact',
    elements                         : 'rteField',
    width                            : '100%',
    height                           : '500',
    language                         : 'ru',
    element_format                   : 'xhtml',
    schema                           : 'html4',
    paste_text_use_dialog            : true,
    document_base_url                : sgConfig._modxSiteUrl,
    relative_urls                    : true,
    remove_script_host               : true,
    convert_urls                     : true,
    force_br_newlines                : false,
    force_p_newlines                 : true,
    forced_root_block                : 'p',
    valid_elements                   : mce_valid_elements,
    popup_css_add                    : '/assets/plugins/tinymce/style/popup_add.css',
    theme_advanced_source_editor_height : 400,
    accessibility_warnings : false,
    theme_advanced_toolbar_location  : 'top',
    theme_advanced_statusbar_location: 'bottom',
    theme_advanced_toolbar_align     : 'ltr',
    theme_advanced_font_sizes        : '80%,90%,100%,120%,140%,160%,180%,220%,260%,320%,400%,500%,700%',
    content_css                      : '/assets/plugins/tinymce/style/content.css',
    formats : {
        alignleft   : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'justifyleft'},
        alignright  : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'justifyright'},
        alignfull   : {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes : 'justifyfull'}
    },
    apply_source_formatting          : true,
    remove_linebreaks                : false,
    convert_fonts_to_spans           : true,
    plugins                          : 'visualblocks,autolink,inlinepopups,autosave,save,advlist,style,fullscreen,advimage,paste,advlink,media,contextmenu,table,youtubeIframe',
    theme_advanced_buttons1          : 'undo,redo,|,bold,forecolor,backcolor,strikethrough,formatselect,fontsizeselect,pastetext,pasteword,code,|,fullscreen,help',
    theme_advanced_buttons2          : 'image,media,youtubeIframe,link,unlink,anchor,|,justifyleft,justifycenter,justifyright,|,bullist,numlist,|,blockquote,outdent,indent,|,table,hr,|,visualblocks,styleprops,removeformat',
    theme_advanced_buttons3          : '',
    theme_advanced_buttons4          : '',
    theme_advanced_resize_horizontal :  false,
    external_link_list_url           : '/assets/plugins/tinymce/js/tinymce.linklist.php',
    template_external_list_url       : '/assets/plugins/tinymce/js/get_template.php',
    template_popup_width             : 550,
    template_popup_height            : 350,
    theme_advanced_blockformats      : 'p,h1,h2,h3,h4,h5,h6,div,blockquote,code,pre',
    theme_advanced_styles            : 'left=justifyleft;right=justifyright',
    theme_advanced_disable           : '',
    theme_advanced_resizing          : true,
    fullscreen_settings : {
        theme_advanced_buttons1_add_before : 'save'
    },
    plugin_insertdate_dateFormat     : '%d-%m-%Y',
    plugin_insertdate_timeFormat     : '%H:%M:%S',
    entity_encoding                  : 'named',
    file_browser_callback            : 'mceOpenServerBrowser',
    paste_text_sticky                : true,
    setup : function(ed)
    {
        ed.onPostProcess.add(function(ed, o)
        {
            // State get is set when contents is extracted from editor
            if (o.get)
            {
                o.content = o.content.replace('<p>{' + '{', '{' + '{');
                o.content = o.content.replace('}}</p>', '}}');
                o.content = o.content.replace(/<p>\[([\[\!\~\^])/g, '[$1');
                o.content = o.content.replace(/([\]\!\~\^])\]<\/p>/g, '$1]');
            }
        });
    },
    onchange_callback                : false,
    valid_elements : "*[*]"
})
}
})(jQuery)

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

Теперь создаем в папке assets/plugins/simplegallery/js/ файл custom.json:

{
    "scripts": {
        "editFormMebelTemplate":{
            "version":"1.0.0",
            "src":"assets/plugins/simplegallery/js/tpl/editFormMebel.js"
        },
        "editForm":{
            "version":"1.0.0",
            "src":"assets/plugins/simplegallery/js/plugin/editForm.js"
        },
        "sgTinyMCE" : {
            "version":"0.0.0",
            "src":"assets/plugins/simplegallery/js/plugin/tinymce.js"
        },
        "rteForm" : {
            "version":"1.4.1",
            "src":"assets/plugins/simplegallery/js/tpl/rteForm.js"
        }
    }
}

Если есть файл custom.json, то SimpleGallery загружает из него скрипты дополнительно к скриптам, описанным в файле scripts.json. Аналогично загружаются и css-файлы.

На этом работа закончена. Итак, измененная форма:


Вкладка с дополнительными полями:


Редактор:


Редактор описания мне понадобился сегодня для другого сайта, поэтому я решил положить его в коробку (: Так что если кому-то вдруг нужно, но разбираться лень, то достаточно переименовать файл _custom.json.

9 комментариев

avatar
Добрый день.
Все же если можно — пару слов по node.js (а то гугл уже выдохся).
Скачал node.js с официального сайта (пробовал и lts версию и latest) с расширением msi для своей ОС Windows 7 32 bit. Установил по умолчанию.
Запускаю из командной строки Node (как рекомендуют для винды), команда
npm install -g handlebars
выполняется, handlebars инициализируется в c:\Users\admin\AppData\Roaming\npm\ (хотя этой директории реально нет).
Перехожу на локалку (денвер) в проект в директорию проекта assets/plugins/simplegallery/js/tpl/
Командую
handlebars *.handlebars -f templates.js -m

и вот здесь выдается ошибка, что не удалось открыть файл шаблона.
Скрин

Создал директорию c:\Users\admin\AppData\Roaming\npm\ и загрузил в нее файл editFormShop.handlebars — результат тот же.

Вопрос. Нужно ли до команды компилирования создавать какие-то директории (проекты), подгружать какие-то узлы, где изначально должны находиться файлы handlebars.js и editFormShop.handlebars. В общем, что не так, ткните носом.

Спасибо.
  • paic
  • 0
avatar
Да, так не работает почему-то. Я все эти команды выполнял в Git Bash (поставился с гитом) прямо в папке SG, все работает.
avatar
спасибо, буду разбираться дальше
avatar
Наконец дошли руки, почитал первые два урока, интересно.
Правда, до Node.js и до усов еще не касался.

Мне бы для начала с GitHub разобраться. Пока не могу понять вот что:
Я скачал Github Desktop с Гитхаба, пробую.
Прочитав ваши комменты, ввел в поиск Гугла «Git Bash» и нашел Git for Windows и Git SCM.

Это все разновидности софта для одной и той же цели? или что-то иное? пока не пойму.
Есть ли где-то толковое общее объяснение? чтобы прочитать, понять общую картину, а уже потом переходить к изучению деталей. Киньте ссылку плиз, если есть такая.
Комментарий отредактирован 2016-07-05 18:17:47 пользователем Aharito
avatar
Да, софт разный, а цель одна. Только при чем тут гит? Проблема же в другом совсем была и то, не понятно в чем (:
avatar
Все верно, гит ни при чем. Небольшой оффтоп у меня получился ) Руководство я уже нашёл, буду читать.
avatar
Для быстрого старта сгодится такая инструкция:
github.com/andreiled/mipt-cs-4sem/wiki/%D0%9F%D0%BE%D1%88%D0%B0%D0%B3%D0%BE%D0%B2%D0%B0%D1%8F-%D0%B8%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%86%D0%B8%D1%8F-%D0%BF%D0%BE-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B5-%D1%81-git-%D0%B8-github-%D0%B4%D0%BB%D1%8F-%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82%D0%BE%D0%B2
Лично у меня все к тому и сводится: git clone — git add. — git commit — git push. Хотя есть подозрение, что я как-то неправильно все делаю (:
avatar
Я тоже до сих пор базовыми вещами Гитхаба и Гитхаб-клиента пользовался, безо всякого обучения, чисто интуитивно. Но хочется на нормальном уровне использовать инструмент.
Для начала решил почитать вот это.

P.S. Спасибо за ссылку.
Комментарий отредактирован 2016-07-06 05:39:23 пользователем Aharito
avatar
Подскажите, пожалуйста, какой правильный запрос сниппета if сделать, чтобы проверить, пуста ли галерея? (выводить только если не пуста)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.