Search code examples
symfonyckeditorelfinder

Using ElFinder on Symfony: I can't select images


I want to use ElFinder as my file manager in my Symfony project. I followed the doc on GitHub. In my routes.yaml:

elfinder:
     resource: '@FMElfinderBundle/Resources/config/routing.yaml'

security.yaml

- { path: ^/efconnect, role: [ROLE_USER] }
- { path: ^/elfinder, role: [ROLE_USER] }

And finally on fm_elfinder.yaml

fm_elfinder:
    instances:
        default:
            locale: '%locale%' # defaults to current request locale
            editor: ckeditor # other options are tinymce, tinymce4, fm_tinymce, form, simple, custom
            connector:
                roots:
                    uploads:
                        driver: LocalFileSystem
                        path: uploads
                        upload_max_size: 2M

Then I added it on a Admin element on Sonata, like this: protected function configureFormFields(FormMapper $formMapper)

{
    $formMapper
        ->with('Contenu')
        ->add('published', CheckboxType::class, ['required' => false, 'label' => 'Publier'])
        ->add('title', TextType::class, ['required' => true, 'label' => 'Titre'])
        ->add('textLink', TextType::class, ['required' => true, 'label' => 'Texte du lien'])
        ->add('media', ElFinderType::class, array(
            'label' => 'Photo',
            'enable' => true,
            'required' => true,
            'instance' => 'default',
            'attr' => array('class' => 'form-control')
            )
        )
        ->end();
}

Then I go on Sonata Admin, and when I try to add an image, a window open, I added an jpeg but then when i click on it, nothing seems to happen. Like, I can select it but my window stay open and my field doesn't fill up with the name of the image. Thanks for your help.


Solution

  • Ok I got it to work on my easyadmin forms. This is the easyAdmin 2 configuration :

    easy_admin:
      entities:
        BlogPost:
          class: App\Entity\BlogPost
          form:
            fields:
              - { property: 'title', label: 'page.title' }
              - { property: 'image', type: 'App\Form\ElFinderType', label: 'blog.image.file', type_options: { enable: true, instance: 'single' } }
    

    (I copied the vendor/helios-ag/fm-elfinder-bundle/src/Form/Type/ElFinderType.php file to App\Form\ElFinderType.php. This is not required).

    I set the fm_elfinder connector to simple :

    # src/packages/fm_elfinder.yaml
    fm_elfinder:
      instances:
        single:
          locale: fr
          editor: simple
          theme: smoothness
          relative_path: false
          connector:
            roots:
              uploads:
                driver: LocalFileSystem
                path: uploads/images
                show_hidden: false
                upload_allow: [ 'image/png', 'image/jpg', 'image/jpeg' ]
                upload_deny: [ 'all' ]
                upload_max_size: 2M
    

    Next I copied two of the template files from the bundle to my templates\bundles directory :

    {# templates/bundles/FMElfinderBundle/Form/elfinder_widget.html.twig #}
    {% block elfinder_widget %}
        <div class="buttons">
                <input type="button" {{ block('widget_attributes') }}
                             data-type="elfinder-remove-button" value="Delete" class="{% if value is empty %}hide{% endif %} remove-button"/>
                <input type="button" {{ block('widget_attributes') }}
                             data-type="elfinder-add-button" value="Add" class="{% if value is not empty %}hide{% endif %} add-button"/>
        </div>
        {% if value is not empty %}
        <img src="{{ value }}" alt="">
        {% endif %}
        <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}
                     data-type="elfinder-input-field" class="hide"/>
    
        {% if enable and instance is defined %}
            <script type="text/javascript" charset="utf-8">
    
                const addButtonSelector = '[data-type="elfinder-add-button"][id="{{ id }}"]';
                const removeButtonSelector = '[data-type="elfinder-remove-button"][id="{{ id }}"]';
                const inputSelector = '[data-type="elfinder-input-field"][id="{{ id }}"]';
    
                live('click', addButtonSelector, () => {
                    window.open("{{ path('elfinder', {'instance': instance, 'homeFolder': homeFolder }) }}?id={{ id }}", "popupWindow", "height=450, width=900");
                });
    
                removeButtonListener()
    
                function live(eventType, elementQuerySelector, cb) {
                    document.addEventListener(eventType, function (event) {
                        const qs = document.querySelectorAll(elementQuerySelector);
                        if (qs) {
                            let el = event.target, index = -1;
                            while (el && ((index = Array.prototype.indexOf.call(qs, el)) === -1)) {
                                el = el.parentElement;
                            }
                            if (index > -1) {
                                cb.call(el, event);
                            }
                        }
                    })
                }
    
                function setValue(value) {
                    document.querySelector(inputSelector).value = value;
                }
    
                function displayImage(value) {
                    const inputElement = document.querySelector(inputSelector);
                    const parent = inputElement.parentElement
                    const image = document.createElement("img");
                    image.src = value;
                    parent.append(image)
                    const removeButton = document.querySelector(removeButtonSelector);
                    const addButton = document.querySelector(addButtonSelector);
                    removeButton.classList.remove('hide')
                    addButton.classList.add('hide')
                    removeButtonListener()
                }
    
                function removeButtonListener() {
                    const removeButtonElement = document.querySelector(removeButtonSelector);
                    if(removeButtonElement){
                        removeButtonElement.addEventListener('click', () => {
                            const addButtonElement = document.querySelector(addButtonSelector);
                            removeButtonElement.classList.remove('hide')
                            const parent = removeButtonElement.closest('.form-widget')
                            const inputElement = parent.querySelector('[data-type="elfinder-input-field"]');
                            inputElement.value = null
                            const imageElement = parent.getElementsByTagName('img')[0]
                            if (imageElement) {
                                parent.removeChild(imageElement)
                            }
                            removeButtonElement.classList.add('hide')
                            addButtonElement.classList.remove('hide')
                            document.querySelector(inputSelector).value = null;
                        })
                    }
                }
            </script>
        {% endif %}
    {% endblock %}
    

    This first one contains the fields and the JavaScript needed to fill them in. I added a Add button. This will open the elfinder, once an image gets selected this button becomes hidden and a Remove button will be added.

    Next is the ElFinder window :

    {# templates/bundles/FMElfinderBundle/Elfinder/simple.html.twig #}
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
        <script data-main="{{ path('ef_main_js') }}"
                        src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
        <script>
            function getUrlParam(paramName) {
                const reParam = new RegExp('(?:[\?&]|&amp;)' + paramName + '=([^&]+)', 'i');
                const match = window.location.search.match(reParam);
    
                return (match && match.length > 1) ? match[1] : '' ;
            }
            define('elFinderConfig', {
                // elFinder options (REQUIRED)
                // Documentation for client options:
                // https://github.com/Studio-42/elFinder/wiki/Client-configuration-options
                defaultOpts: {
                    url: '{{ path('ef_connect', { 'instance': instance, 'homeFolder': homeFolder } ) }}',
                    lang: '{{ locale }}',
                    onlyMimes: {{ onlyMimes|raw }},
                    getFileCallback: function (file) {
                        window.opener.setValue(file.url);
                        window.opener.displayImage(file.url);
                        window.close();
                    },
                    commandsOptions: {
                        edit: {
                            extraOptions: {
                                // set API key to enable Creative Cloud image editor
                                // see https://console.adobe.io/
                                creativeCloudApiKey: '',
                                // browsing manager URL for CKEditor, TinyMCE
                                // uses self location with the empty value
                                managerUrl: ''
                            }
                        },
                        quicklook: {
                            // to enable CAD-Files and 3D-Models preview with sharecad.org
                            sharecadMimes: ['image/vnd.dwg', 'image/vnd.dxf', 'model/vnd.dwf', 'application/vnd.hp-hpgl', 'application/plt', 'application/step', 'model/iges', 'application/vnd.ms-pki.stl', 'application/sat', 'image/cgm', 'application/x-msmetafile'],
                            // to enable preview with Google Docs Viewer
                            googleDocsMimes: ['application/pdf', 'image/tiff', 'application/vnd.ms-office', 'application/msword', 'application/vnd.ms-word', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/postscript', 'application/rtf'],
                            // to enable preview with Microsoft Office Online Viewer
                            // these MIME types override "googleDocsMimes"
                            officeOnlineMimes: ['application/vnd.ms-office', 'application/msword', 'application/vnd.ms-word', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.presentation']
                        }
                    },
                    // bootCallback calls at before elFinder boot up
                    bootCallback: function (fm, extraObj) {
                        /* any bind functions etc. */
                        fm.bind('init', function () {
                        });
                        // for example set document.title dynamically.
                        var title = document.title;
                        fm.bind('open', function () {
                            var path = '',
                                cwd = fm.cwd();
                            if (cwd) {
                                path = fm.path(cwd.hash) || null;
                            }
                            document.title = path ? path + ':' + title : title;
                        }).bind('destroy', function () {
                            document.title = title;
                        });
                    }
                },
                managers: {
                    // 'DOM Element ID': { /* elFinder options of this DOM Element */ }
                    'elfinder': {}
                }
            });
        </script>
    </head>
    <body>
    
    <!-- Element where elFinder will be created (REQUIRED) -->
    <div id="elfinder"></div>
    
    </body>
    </html>
    

    The main thing added here to make it work is the getFileCallback function :

    getFileCallback: function (file) {
                        window.opener.setValue(file.url);
                        window.opener.displayImage(file.url);
                        window.close();
                    },
    

    This now works fine for my single image field and does not affect the CKEditor integration.