OctoberCMS: виджет с ajax-диалогом

Рассмотрим вставку виджета внутрь сторонней бэкэнд-формы и обращение к вставленному виджету из JS для подгрузки контента. На этом примере мы узнаем:

  • что такое виджет в OctoberCMS, зачем он и как его сделать;
  • как прикрепить виджет к контроллеру, определенному в другом плагине;
  • как пользоваться ajax-фреймворком в OctoberCMS.

Виджет для бэк-энда OctoberCMS — это то же самое, что компонент для фронт-энда. То есть это отдельный блок логики и представления, который можно пихать где попало повторно использовать. Вообще, виджеты бывают трёх видов:

  • Form Widget, то есть для использования в формах, для манипуляции данными моделей,
  • Report Widget для вывода полезных данных на панель управления в бэк-энде,
  • и просто Widget — для использования в любом контроллере бэк-энда. Например, Media Manager, красивый диалог для вставки медиа-файлов в текст, реализован в OctoberCMS как виджет.

В данной заметке мы будем делать ajax-диалог (который потом превратится в диалог выбора фотографий и вставки специального маркдаун-кода выбранной фотографии в текст). То есть нам нужен просто Widget. Создадим файл PhotoSelector.php в директории widgets нашего плагина:

<?php

namespace Graker\PhotoAlbums\Widgets;
use Backend\Classes\WidgetBase;

class PhotoSelector extends WidgetBase {

    protected $defaultAlias = 'photoSelector';

    public function render() {
        return $this->makePartial('body');
    }

    protected function loadAssets() {
        $this->addJs('js/photoselector.js');
        $this->addCss('css/photoselector.css');
    }

}

Метод render() обычно используется для вывода виджета (см. ниже), а loadAssets() — для добавления скриптов, необходимых виджету. Специально вызывать loadAssets() не нужно, метод вызовется автоматически.

Теперь прикрепим созданный виджет к контроллеру формы редактирования заметки в блог. Поскольку эта форма определена в другом плагине (RainLab.Blog), воспользуемся событием backend.form.extendFields и прикрепим виджет из метода boot() нашего плагина:

public function boot() {
    Event::listen('backend.form.extendFields', function (Form $widget) {
        $controller = $widget->getController();
        if (!($controller instanceof \RainLab\Blog\Controllers\Posts)) {
            return ;
        }
        if (!($widget->model instanceof \RainLab\Blog\Models\Post)) {
            return ;
        }
        $photo_selector = new PhotoSelector($controller);
        $photo_selector->alias = 'photoSelector';
        $photo_selector->bindToController();
        // заодно вставим скрипт, который добавляет в редактор новую кнопку
        $widget->addJs('/plugins/graker/photoalbums/assets/js/extend-markdown-editor.js');
    });
}

Это всё, теперь виджет доступен контроллеру формы. Обратите внимание, что при прикреплении мы указали alias — это будет уникальный идентификатор прикрепленного виджета. Он нам позже понадобится.

Обычно прикрепленный виджет нужно рендерить в представлении контроллера вызовом $widget->render(). Но раз мы делаем диалог, который появляется не сразу, рендерить его мы будем потом, когда пользователь нажмет на соответствующую кнопку.

Как добавить кнопку в редактор постов, я уже писал раньше, но повторенье, как известно, мать ученья:

+function ($) {

    $(document).ready(function () {
        var editor = $('[data-control="markdowneditor"]').data('oc.markdownEditor');

        var button = {
            label: 'Insert photo from Photoalbums',
            cssClass: 'oc-autumn-button oc-icon-camera-retro',
            insertAfter: 'mediaimage',
            action: 'showAlbumsDialog',
            template: '$1'
        };

        editor.showAlbumsDialog = function (template) {
            var editor = this.editor,
                pos = this.editor.getCursorPosition();

            new $.oc.photoselector.popup({
                alias: 'photoSelector',
                onInsert: function (code, album) {
                    // коллбэк когда нажата кнопка "Вставить" в диалоге
                    // здесь будет код, который заменяет текст в редакторе вставленной фотокарточкой
                }
            });
        };

        //add button to editor
        editor.addToolbarButton('photoalbums', button);
    });

}(window.jQuery);

По нажатию на нашу новую кнопку вызывается функция showAlbumsDialog, в которой будет создан новый диалог. Среди параметров, передаваемых в диалог — alias виджета, к которому мы будем обращаться за данными, и код, который нужно выполнить, когда пользователь нажмёт на кнопку «Вставить» нашего предполагаемого диалога.

Конечно, чтобы этот код заработал и диалог появился, его нужно сначала определить:

+function () {

    if ($.oc.photoselector === undefined) {
        $.oc.photoselector = {};
    }

    var Base = $.oc.foundation.base,
        BaseProto = Base.prototype;

    var PhotoSelector = function (options) {
        this.$dialog = $('<div/>');
        this.options = $.extend({}, PhotoSelector.DEFAULTS, options);

        Base.call(this);

        this.show();
    };

    PhotoSelector.prototype = Object.create(BaseProto);
    PhotoSelector.prototype.constructor = PhotoSelector;

    PhotoSelector.prototype.show = function () {
        this.$dialog.one('complete.oc.popup', this.proxy(this.onPopupShown));
        this.$dialog.popup({
            size: 'large',
            extraData: {album: this.options.album },
            handler: this.options.alias + '::onDialogOpen'
        });
    };

    // ...

    PhotoSelector.DEFAULTS = {
        alias: undefined,
        album: 0,
        onInsert: undefined
    };

    $.oc.photoselector.popup = PhotoSelector;

} (window.jQuery);

При создании диалога используется объект Base, определенный в JS-фреймворке OctoberCMS. Он нужен, чтобы можно было воспользоваться методом proxy(), который позволяет передавать методы объекта в качестве обработчиков событий.

В методе show() мы привязываемся к событию complete.oc.popup (диалог готов и его контент загружен). А затем, собственно, открываем диалог с помощью метода popup(), в который мы передаем несколько настроек: размер диалогового окна, дополнительные данные (передадутся на сервер) и самое главное — название метода на стороне сервера, который вернёт нам содержимое, то есть разметку диалога.

Как известно из описания ajax-фреймворка OctoberCMS, мы можем обращаться из JS к методам контроллера, чтобы загрузить тот или иной partial. Так вот, штука в том, что поскольку мы прикрепили наш виджет к контроллеру, теперь к методам виджета мы тоже можем обращаться из JS. Только к имени метода нужно добавить alias виджета, вот так: widgetAlias::onMyEvent. Что мы и делаем в данном примере.

А вот обработчик события, которое возникает, когда диалог готов.

PhotoSelector.prototype.onPopupShown = function (event, element, popup) {
    this.$dialog = popup;
    // в данном случае мы добавляем обработчик клика по кнопке "Вставить"
    $('div.photo-selection-dialog').find('button.btn-insert').click(this.proxy(this.onInsertClicked));
};

Кстати, событие complete.oc.popup — не совсем задокументированное, его пока можно найти только в коде ajax-фреймворка OctoberCMS. Но пользоваться нужно именно им, так как именно это событие срабатывает, когда диалог не только показан, но и его контент уже успешно загружен с сервера, так что мы уже можем добавить обработчики к кнопкам и т.д.

Остается только реализовать серверную часть в классе виджета:

class PhotoSelector extends WidgetBase {

    public function render() {
        return $this->makePartial('body');
    }

    // ...

    public function onDialogOpen() {
        return $this->render();
    }

}

Вот и все, при обращении из JS наш класс отрендерит нужный паршл и вернет его клиенту. Остальное за нас сделает ajax-фреймворк. К слову, если возвращать из метода массив, в котором ключи — это id элементов, а значения — фрагменты верстки (то есть паршлы), фреймворк может обновить отдельные части страницы по указанным id. Эта фича хорошо описана в документации.

Также стоит заметить, что если мы из JS передали на сервер дополнительные данные (см. extraData: {album: this.options.album } выше), то их можно получить в коде виджета как обычные данные из POST-запроса:

$album_id = input('album');

А паршл, который рендерит виджет, должен быть в файле widgets/photoselector/_body.htm:

<div class="modal-lg modal-dialog photo-selection-dialog">
    <div class="modal-content">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal">×</button>
            <h4>Выбираем фото</h4>
        </div>
        <div id="listContainer" class="modal-body">
            Здесь будем выводить альбомы и фотографии для выбора
        </div>
        <div class="modal-footer">
            <button class="btn btn-primary btn-insert">Вставить</button>
            <button class="btn btn-default btn-cancel" data-dismiss="modal">Отмена</button>
        </div>
    </div>
</div>

В целом, пользоваться ajax-фреймворком в OctoberCMS легко и приятно. Его реализация позволяет пользователям вообще не заморачиваться с ajax-запросами, а сосредоточиться на своей логике. То есть на том, как, собственно, должен работать наш диалог — загружать альбомы, выбирать из них фотокарточки и т.д. Реализацию диалога (на основе изложенного в заметке) вы можете найти в моём плагине PhotoAlbums на гитхабе или в маркете плагинов. Выглядит он примерно так:

Диалог выбора фотокарточек

Комментарии