Laravel-Backpack / community-forum

A workspace to discuss improvement and feature ideas, before they're actually implemented.
28 stars 0 forks source link

[Proposal] Select2 from ajax sortable #32

Open Mamatlatomate opened 4 years ago

Mamatlatomate commented 4 years ago

Hello everyone,

I recently need to make a sortable list based on ajax call and potentially contextualized by another field. So i tried something like this :

@php
    $connected_entity = new $field['connected_entity'];
    $connected_entity_key_name = $connected_entity->getKeyName();
    $old_value = old(square_brackets_to_dots($field['name'])) ?? $field['value'] ?? $field['default'] ?? false;

    $field['delay'] = $field['delay'] ?? 500;
@endphp

<div @include('crud::inc.field_wrapper_attributes') >
    <label>{!! $field['label'] !!}</label>
    @include('crud::inc.field_translatable_icon')
    <select
        name="{{ $field['name'] }}[]"
        style="width: 100%"
        id="select2_ajax_multiple_{{ $field['name'] }}"
        data-init-function="bpFieldInitSelect2FromAjaxMultipleElement"
        data-column-nullable="{{ $crud->model::isColumnNullable($field['name']) ? 'true' : 'false' }}"
        data-dependencies="{{ isset($field['dependencies']) ? json_encode(array_wrap($field['dependencies'])) : json_encode([]) }}"
        data-placeholder="{{ $field['placeholder'] }}"
        data-minimum-input-length="{{ $field['minimum_input_length'] }}"
        data-data-source="{{ $field['data_source'] }}"
        data-method="{{ $field['method'] ?? 'GET' }}"
        data-field-attribute="{{ $field['attribute'] }}"
        data-connected-entity-key-name="{{ $connected_entity_key_name }}"
        data-include-all-form-fields="{{ isset($field['include_all_form_fields']) ? ($field['include_all_form_fields'] ? 'true' : 'false') : 'true' }}"
        data-ajax-delay="{{ $field['delay'] }}"
        @include('crud::inc.field_attributes', ['default_class' =>  'form-control'])
        multiple>

        @if ($old_value)
            @foreach ($old_value as $item)
                @if (!is_object($item))
                    @php
                        $item = $connected_entity->find($item);
                    @endphp
                @endif
                <option value="{{ $item->getKey() }}" selected>
                    {{ $item->{$field['attribute']} }}
                </option>
            @endforeach
        @endif
    </select>

    {{-- HINT --}}
    @if (isset($field['hint']))
        <p class="help-block">{!! $field['hint'] !!}</p>
    @endif
</div>

@if ($crud->fieldTypeNotLoaded($field))
    @php
        $crud->markFieldTypeAsLoaded($field);
    @endphp

    @push('crud_fields_styles')
        <link href="{{ asset('packages/select2/dist/css/select2.min.css') }}" rel="stylesheet" type="text/css" />
        <link href="{{ asset('packages/select2-bootstrap-theme/dist/select2-bootstrap.min.css') }}" rel="stylesheet" type="text/css" />

        <style>
            li.select2-selection__choice:hover {
                cursor: pointer!important;
            }
        </style>
    @endpush

    @push('crud_fields_scripts')
        <script src="{{ asset('packages/select2/dist/js/select2.full.min.js') }}"></script>
        <script src="{{ asset('packages/jquery-ui-dist/jquery-ui.min.js') }}"></script>
        @if (app()->getLocale() !== 'en')
            <script src="{{ asset('packages/select2/dist/js/i18n/' . app()->getLocale() . '.js') }}"></script>
        @endif
    @endpush
@endif

@push('crud_fields_scripts')
    <script>
        function bpFieldInitSelect2FromAjaxMultipleElement(element) {
            let form = element.closest('form');
            let $placeholder = element.attr('data-placeholder')
            let $minimumInputLength = element.attr('data-minimum-input-length')
            let $dataSource = element.attr('data-data-source');
            let $method = element.attr('data-method');
            let $fieldAttribute = element.attr('data-field-attribute');
            let $connectedEntityKeyName = element.attr('data-connected-entity-key-name');
            let $includeAllFormFields = element.attr('data-include-all-form-fields') !== 'false';
            let $allowClear = element.attr('data-column-nullable') === 'true';
            let $dependencies = JSON.parse(element.attr('data-dependencies'));
            let $ajaxDelay = element.attr('data-ajax-delay');

            var orderSortedValues = function() {
                $(element).parent().find("ul.select2-selection__rendered").children("li[title]").each(function(i, obj) {
                    var el = $(element).children('option').filter(function () { return $(this).html() === obj.title });
                    moveElementToEndOfParent(el)
                });
            };

            var moveElementToEndOfParent = function(element) {
                var parent = element.parent();
                element.detach();
                parent.append(element);
            };

            if (!$(element).hasClass("select2-hidden-accessible"))
            {
                $(element).select2({
                    theme: 'bootstrap',
                    multiple: true,
                    placeholder: $placeholder,
                    minimumInputLength: $minimumInputLength,
                    allowClear: $allowClear,
                    ajax: {
                        url: $dataSource,
                        type: $method,
                        dataType: 'json',
                        delay: $ajaxDelay,
                        data: function (params) {
                            if ($includeAllFormFields) {
                                return {
                                    q: params.term, // search term
                                    page: params.page, // pagination
                                    form: form.serializeArray() // all other form inputs
                                };
                            } else {
                                return {
                                    q: params.term, // search term
                                    page: params.page, // pagination
                                };
                            }
                        },
                        processResults: function (data, params) {
                            params.page = params.page || 1;

                            return {
                                results: $.map(data.data, function (item) {
                                    return {
                                        text: item[$fieldAttribute],
                                        id: item[$connectedEntityKeyName]
                                    }
                                }),
                                pagination: {
                                    more: data.current_page < data.last_page
                                }
                            };
                        },
                        cache: true
                    },
                });
                $("ul.select2-selection__rendered").sortable({
                    containment: 'parent',
                    update: function () {
                        orderSortedValues()
                    }
                });
            }

            for (var i=0; i < $dependencies.length; i++) {
                $dependency = $dependencies[i];
                $('input[name='+$dependency+'], select[name='+$dependency+'], checkbox[name='+$dependency+'], radio[name='+$dependency+'], textarea[name='+$dependency+']').change(function () {
                    element.val(null).trigger("change");
                });
            }
        }
    </script>
@endpush

Basically it's a classic select2_from_ajax widget but which is not saving a relationship. The column need to be a text or json to store the result exactly the same way that the select_and_order field. So it's going to save an array of id's (or model primaryKey) based on the connected_entity.

Sources : To create this, I used the select_2_from_ajax from laravel backpack, turn it to multiple and use this example for turning it to sortable : http://jsfiddle.net/karthi3788/cveekdxy/2/

I don't know if it's an interesting widget to integrate to backpack but I wanted to share it with you. Hope it will help someone :)

tabacitu commented 4 years ago

Uuu! This is so cool @Mamatlatomate - thanks a lot for sharing!

We're hard at work on 4.1 right now, so I can't give this the attention it deserves, but I'll make a note to take a look at this after we launch 4.1 - and see how and if we should integrate it. Until then, I'm sure some people will run into your code and be grateful, thanks for sharing.

Cheers!