Summernote — «перезагрузка»

По умолчанию этот редактор, при добавлении изображения в контент, преобразует его в base64. Но при добавлении этого «счастья» в базу данных, даже поля типа TEXT может оказаться недостаточным и со временем БД может увеличится до невероятных размеров.

summernote-1

К счастью, разработчики предусмотрели вариант перехвата изображении callback функцией onImageUpload, чем можно воспользоваться для того, чтобы вклиниться в процесс загрузки и передать управление бэкэнду.

// summernote-config.js

$(document).ready(function () {
    function uploadFiles(files, editor) {
        for (var i = 0; i < files.length; i++) {
            uploadFileOnServer(files[i], editor);
        }
    }

    function uploadFileOnServer(image, editor) {
        var formData = new FormData();
        formData.append('file', image);
        $.ajax({
            data: formData,
            type: "POST",
            url: "URL php-обработчика",
            contentType: false,
            processData: false,
            success: function (url) {
                var image = $('<img>').attr('src', url.fullFilePath);
                editor.summernote("insertNode", image[0]);
            }
        });
    }

    $('#summernote').summernote(
        {
            callbacks: {
                onImageUpload: function (files) {
                    uploadFiles(files, $(this));
                }
            }
        }
    );
});

Ловим каждый файл на стороне сервера и физически перемещаем файл в нужную директорию, возвращаем json ответ, который представляет полный путь к картинке. В зависимости от экосистемы бэкэнда, перемещение файлов может осуществляться как нативными функциями php, так и с использованием функции фреймворка, например, Symfony.

В контексте SF (Symfony Framework), можно воспользоваться пакетом FosJSRouting, для того чтобы обращаться к именам роутов контролера из js кода, примеры использования. Поэтому, после установки и настройки пакета, вместо "URL php-обработчика", куда будет отправляться картинка, мы запишем:

Routing.generate("summernote_upload")

В данном случае, имя роута summernote_upload и далее можно смело писать контролер UploadImageController.php, отвечающий за обработку файлов на сервере:

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class UploadImageController extends Controller
{
    /**
     * @Route("/upload", options={"expose"=true}, methods={"POST"}, name="summernote_upload")
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function uploadImageAction(Request $request)
    {
        /** @var $file UploadedFile */
        $file = $request->files->get('file');
        $fileName = md5(uniqid()) . '.' . $file->guessExtension();
        
        // summernote.upload_dir для удобства выносится в параметры
        $relPath = $this->getParameter('summernote.upload_dir');
        
        $absPath = \dirname($this->get('kernel')->getRootDir()) . '/web' . $relPath;
        $fs = new Filesystem();
        if (!$fs->exists($absPath)) {
            $fs->mkdir($absPath);
        }
        $file->move(
            $absPath,
            $fileName
        );
        return new JsonResponse([
            'fullFilePath' => $relPath . $fileName
        ]);
    }
}

Пример на SF показывает как легко можно обработать загрузку файлов для этого редактора. Посредством ajax обращения сразу к нужному контролеру, загрузки файла с использованием Filesystem компонента фреймворка и возвращением результата с последующей вставкой картинки в редактор с указанием полного пути без перезагрузки страницы.

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

Создание плагина для редактора и загрузка файлов

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

uploadFile

uploadFile-2

Это действие будет загружать любой файл, а не только изображения (как в случае с кнопкой "вставить изображение"). Идея такая, что нам нужно подключить новый js-файл, а в том месте, где происходит инициализация плагина, прописать в конфиг summernote название плагина uploadFile и зарегистрировать ещё один коллбэк onFileUpload (логику обработки файлов на сервере и вывод на клиента здесь приводить не буду, покажу просто связку плагина и перехват данных в файле инициализации редактора, остальное доработать уже дело техники).

// summernote-config.js

$('#summernote').summernote(
{
   toolbar: [
      // ...
      ['insert', ['uploadFile'/*, ... */]],
      // ...
    ],
   minHeight: 150,
   callbacks: {
      // ...
      onFileUpload: function (data) {
         // дальше уже реализуем uploadFiles функцию исходя из предпочтении
         // data - это FormData, содержащий файл и читаемую ссылку
         // Routing.generate("summernote_file_upload") - это просто серверный роут (где хранится каталог загруженных файлов)

         uploadFiles(data, editor, Routing.generate("summernote_file_upload"))
      }
   },
});
// summernote-plugin-file-upload.js

(function (factory) {
    // generic dependency detection method
    /* global define */
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals
        factory(window.jQuery);
    }
}(function ($) {

    var contextGlobal = {};
    var layoutInfo = {};
    var $editor = {};
    var options = {};
    var $dialog = {};

    var uploadFileFormSelector = 'upload-file-form-plugin';
    var uploadFileInputSelector = 'upload-file-input-file-plugin';
    var uploadFileNameSelector = 'upload-file-input-name-plugin';
    var fileObject = {
        files: '',
        readableName: '',
    };

    // Extends plugins for adding uploadFile function in non-multiple mode
    //  - plugin is external module for customizing.
    $.extend($.summernote.plugins, {
        /**
         * @param {Object} context - context object has status of editor.
         */
        'uploadFile': function (context) {
            var self = this;

            contextGlobal = context;
            layoutInfo = context.layoutInfo;
            $editor = layoutInfo.editor;
            options = context.options;

            // ui has renders to build ui elements.
            //  - you can create a button with `ui.button`
            var ui = $.summernote.ui;
            addUploadFileButton(contextGlobal, ui, self);

            init(this, ui);
        }
    });

    function addUploadFileButton(context, ui, plugin) {
        context.memo('button.uploadFile', function () {
            var button = ui.button({
                contents: '<i class="fa fa-upload"/>',
                tooltip: 'Upload file and insert link',
                click: function () {
                    return uploadFileClickHandler(plugin);
                }
            });
            return button.render();
        });
    }

    function init(plugin, ui) {

        plugin.initialize = function () {
            var body = getModalBodyForm();
            renderModalForm(body);
            $('.' + uploadFileFormSelector).on('submit', function (event) {
                event.preventDefault();
                fileObject.readableName = $('.' + uploadFileNameSelector).val();
                plugin.handleForm(fileObject.readableName, fileObject.files[0]);
                ui.hideDialog($dialog);
                $(this).find('input').val('');
                fileObject.readableName = fileObject.files = '';
                contextGlobal.invoke('editor.restoreRange');
            });
        };

        function getModalBodyForm() {
            var footer = '<button type="submit" class="btn btn-primary note-btn note-btn-primary">Insert</button>';
            return [
                '<form class="' + uploadFileFormSelector + '" method="post" name="' + uploadFileFormSelector + '">',
                '<div class="form-group note-form-group note-group-select-from-files">',
                '<label class="note-form-label">Select from files</label>',
                '<input class="' + uploadFileInputSelector + ' form-control-file note-form-control note-input" type="file" name="files" required="required" />',
                '</div>',
                '<div class="form-group note-group-file-name" style="overflow:auto;">',
                '<label class="note-form-label">Text to display</label>',
                '<input class="' + uploadFileNameSelector + ' form-control note-form-control note-input col-md-12" type="text" name="file_name">',
                '</div>',
                footer,
                '</form>',
            ].join('');
        }

        function renderModalForm(body) {
            $dialog = ui.dialog({
                title: 'Insert file',
                fade: options.dialogsFade,
                body: body,
            }).render().appendTo($editor);
        }

        // You should remove elements on `initialize`.
        plugin.destroy = function () {
            ui.hideDialog($dialog);
            $dialog.remove();
        };

        plugin.show = function () {
            ui.showDialog($dialog);
        };

        plugin.handleForm = function (fileName, file) {
            var formData = new FormData();
            formData.append('file_name', fileName);
            formData.append('file', file);
            try {
                contextGlobal.options.callbacks.onFileUpload(formData);
            } catch (e) {
                console.log('Plugin error:' + e.message);
            }
        };
    }

    function uploadFileClickHandler(plugin) {
        contextGlobal.invoke('editor.saveRange');
        plugin.show();

        var $fileInput = $('.' + uploadFileInputSelector);

        // Cloning fileInput to clear element.
        $fileInput.replaceWith($fileInput.clone().on('change', function (event) {
            fileObject.files = event.target.files || event.target.value;
        }));
    }
}));


Похожие заметки:

Корзина на сайте — часть 2

В статье рассказывается как c помощью simpleCart.js делать такие вещи:

  • Выводить товары в том виде, в котором вам надо
  • Обрабатывать переданные товары и возвращать результат
  • Делать дальнейшие операции с заказом

Открыть здесь

Скрипт динамической ширины

Скрипт для равномерного распределения блоков по ширине родительского контейнера. В качестве контейнера может выступать любой блок как определенной ширины, так и неопределенной, вплоть до body. Что умеет?

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

Открыть здесь

Корзина на сайте — часть 1

В статье рассказывается как создать JavaScript корзину на сайте с помощью плагина simpleCart.js

Открыть здесь


Перед тем как писать комментарии, рекомендую ознакомиться:

Markdown синтаксис »

Оформление кода »

Нужна аватарка »

Комментарии


2
avatar

Davi сказал 21-03-2018 в 02:38


А можно как то выводить состояние загрузки?


avatar

Админ

Роман Жариков сказал 21-03-2018 в 09:51


Вполне можно. Достаточно добавить в метод uploadFileOnServer:

$.ajax({
   // ...
   beforeSend: function() {
      // ... делаем что-то
   }
})