jqh 5 tahun lalu
induk
melakukan
ba41a7c782

+ 27 - 1
resources/views/form/hasmany.blade.php

@@ -57,4 +57,30 @@
     </div>
     @endif
 
-</div>
+</div>
+
+<script>
+    var nestedIndex = 0,
+        container = '.has-many-{{ $column }}',
+        forms = '.has-many-{{ $column  }}-forms';
+
+    function replaceNestedFormIndex(value) {
+        return String(value).replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
+    }
+
+    $(container).on('click', '.add', function () {
+
+        var tpl = $('template.{{ $column }}-tpl');
+
+        nestedIndex++;
+
+        var template = replaceNestedFormIndex(tpl.html());
+        $(forms).append(template);
+        {!! $templateScript !!}
+    });
+
+    $(container).on('click', '.remove', function () {
+        $(this).closest('.has-many-{{ $column  }}-form').hide();
+        $(this).closest('.has-many-{{ $column  }}-form').find('.{{ Dcat\Admin\Form\NestedForm::REMOVE_FLAG_CLASS }}').val(1);
+    });
+</script>

+ 50 - 4
resources/views/form/hasmanytab.blade.php

@@ -14,8 +14,8 @@
 <div class="nav-tabs-custom has-many-{{$column}}">
     <div class="row header">
         <div class="{{$viewClass['label']}}"><h4 class="pull-right">{!! $label !!}</h4></div>
-        <div class="{{$viewClass['field']}}">
-            <div class="add btn btn-white btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
+        <div class="{{$viewClass['field']}}" style="margin-bottom: 5px">
+            <div class="add btn btn-outline-primary btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
         </div>
     </div>
 
@@ -53,9 +53,55 @@
         </li>
     </template>
     <template class="pane-tpl">
-        <div class="tab-pane fields-group new" id="{{ $relationName . '_new_' . \Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}">
+        <div class="tab-pane fields-group new" id="{{ $relationName . '_new_' . Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}">
             {!! $template !!}
         </div>
     </template>
 
-</div>
+</div>
+
+<script>
+    var container = '.has-many-{{ $column }}';
+    
+    $(container+' > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
+        var $navTab = $(this).siblings('a');
+        var $pane = $($navTab.attr('href'));
+        if( $pane.hasClass('new') ){
+            $pane.remove();
+        }else{
+            $pane.removeClass('active').find('.{{ Dcat\Admin\Form\NestedForm::REMOVE_FLAG_CLASS }}').val(1);
+        }
+        if($navTab.closest('li').hasClass('active')){
+            $navTab.closest('li').remove();
+            $(container+' > .nav > li:nth-child(1) > a').click();
+        }else{
+            $navTab.closest('li').remove();
+        }
+    });
+
+    var nestedIndex = 0;
+
+    function replaceNestedFormIndex(value) {
+        return String(value).replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
+    }
+
+    $(container+' > .header').off('click', '.add').on('click', '.add', function(){
+        nestedIndex++;
+        var navTabHtml = replaceNestedFormIndex($(container+' > template.nav-tab-tpl').html());
+        var paneHtml = replaceNestedFormIndex($(container+' > template.pane-tpl').html());
+        $(container+' > .nav').append(navTabHtml);
+        $(container+' > .tab-content').append(paneHtml);
+        $(container+' > .nav > li:last-child a').click();
+        {!! $templateScript !!}
+    });
+
+    if ($('.has-error').length) {
+        $('.has-error').parent('.tab-pane').each(function () {
+            var tabId = '#'+$(this).attr('id');
+            $('li a[href="'+tabId+'"] i').removeClass('d-none');
+        });
+
+        var first = $('.has-error:first').parent().attr('id');
+        $('li a[href="#'+first+'"]').tab('show');
+    }
+</script>

+ 27 - 0
resources/views/form/hasmanytable.blade.php

@@ -1,3 +1,6 @@
+<style>
+    .table-has-many .input-group{flex-wrap: nowrap!important}
+</style>
 
 <div class="row form-group">
     <div class="{{$viewClass['label']}} "><label class="control-label pull-right">{!! $label !!}</label></div>
@@ -77,3 +80,27 @@
 
 {{--<hr style="margin-top: 0px;">--}}
 
+<script>
+    var nestedIndex = 0,
+        container = '.has-many-{{ $column }}';
+
+    function replaceNestedFormIndex(value) {
+        return String(value).replace(/{{ Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}/g, nestedIndex);
+    }
+
+    $(container).on('click', '.add', function () {
+        var tpl = $('template.{{ $column }}-tpl');
+
+        nestedIndex++;
+
+        var template = replaceNestedFormIndex(tpl.html());
+        $('.has-many-{{ $column }}-forms').append(template);
+        {!! $templateScript !!}
+    });
+
+    $(container).on('click', '.remove', function () {
+        $(this).closest('.has-many-{{ $column }}-form').hide();
+        $(this).closest('.has-many-{{ $column }}-form').find('.{{ Dcat\Admin\Form\NestedForm::REMOVE_FLAG_CLASS }}').val(1);
+    });
+</script>
+

+ 2 - 0
resources/views/form/multipleselect.blade.php

@@ -19,3 +19,5 @@
 
     </div>
 </div>
+
+@include('admin::form.select-script')

+ 155 - 0
resources/views/form/select-script.blade.php

@@ -0,0 +1,155 @@
+<script require="@select2">
+    var configs = {!! json_encode($configs) !!};
+
+    @if(isset($ajax))
+        configs = $.extend(configs, {
+        ajax: {
+            url: "{{ $ajax['url'] }}",
+            dataType: 'json',
+            delay: 250,
+            data: function (params) {
+                return {
+                    q: params.term,
+                    page: params.page
+                };
+            },
+            processResults: function (data, params) {
+                params.page = params.page || 1;
+
+                return {
+                    results: $.map(data.data, function (d) {
+                        d.id = d.{{ $ajax['idField'] }};
+                        d.text = d.{{ $ajax['textField'] }};
+                        return d;
+                    }),
+                    pagination: {
+                        more: data.next_page_url
+                    }
+                };
+            },
+            cache: true
+        },
+        escapeMarkup: function (markup) {
+            return markup;
+        }
+    });
+    @endif
+
+    @if(isset($remoteOptions))
+    $.ajax({!! json_encode($remoteOptions) !!}).done(function(data) {
+        configs.data = data;
+
+        $("{!! $selector !!}").each(function (_, select) {
+            select = $(select);
+
+            select.select2(configs);
+
+            var value = select.data('value') + '';
+
+            if (value) {
+                select.val(value.split(',')).trigger("change")
+            }
+        });
+    });
+    @else
+    $("{!! $selector !!}").select2(configs);
+    @endif
+</script>
+
+@if(isset($load))
+    <script once>
+        var selector = '{!! $selector !!}';
+
+        $(document).off('change', selector);
+        $(document).on('change', selector, function () {
+            var target = $(this).closest('.fields-group').find(".{{ $load['class'] }}");
+
+            if (String(this.value) !== '0' && ! this.value) {
+                return;
+            }
+            $.ajax("{{ $load['url'] }}?q="+this.value).then(function (data) {
+                target.find("option").remove();
+                $(target).select2({
+                    data: $.map(data, function (d) {
+                        d.id = d.{{ $load['idField'] }};
+                        d.text = d.{{ $load['textField'] }};
+                        return d;
+                    })
+                }).val(target.attr('data-value').split(',')).trigger('change');
+            });
+        });
+        $(selector).trigger('change');
+    </script>
+@endif
+
+@if(isset($loads))
+    {{--loads联动--}}
+    <script once>
+        var selector = '{!! $selector !!}';
+
+        var fields = '{!! $loads['fields'] !!}'.split('^');
+        var urls = '{!! $loads['urls'] !!}'.split('^');
+
+        var refreshOptions = function(url, target) {
+            $.ajax(url).then(function(data) {
+                target.find("option").remove();
+                $(target).select2({
+                    data: $.map(data, function (d) {
+                        d.id = d.{{ $loads['idField'] }};
+                        d.text = d.{{ $loads['textField'] }};
+                        return d;
+                    })
+                }).val(target.data('value').split(',')).trigger('change');
+            });
+        };
+
+        $(document).off('change', selector);
+        $(document).on('change', selector, function () {
+            var _this = this;
+            var promises = [];
+
+            fields.forEach(function(field, index){
+                var target = $(_this).closest('.fields-group').find('.' + fields[index]);
+
+                if (_this.value !== '0' && ! _this.value) {
+                    return;
+                }
+                promises.push(refreshOptions(urls[index] + "?q="+ _this.value, target));
+            });
+
+            $.when(promises).then(function() {});
+        });
+        $(selector).trigger('change');
+    </script>
+@endif
+
+{{--本地化--}}
+<script once>
+    @php
+        $lang = trans('select2');
+        $locale = config('app.locale');
+    @endphp
+    if ($.fn.select2) {
+        var e = $.fn.select2.amd;
+
+        e.define("select2/i18n/{{ $locale }}", [], function () {
+            return {
+                errorLoading: function () {
+                    return "{{ $lang['error_loading'] }}"
+                }, inputTooLong: function (e) {
+                    return "{{ $lang['input_too_long'] }}".replace(':num', e.input.length - e.maximum)
+                }, inputTooShort: function (e) {
+                    return "{{ $lang['input_too_short'] }}".replace(':num', e.minimum - e.input.length)
+                }, loadingMore: function () {
+                    return "{{ $lang['loading_more'] }}"
+                }, maximumSelected: function (e) {
+                    return "{{ $lang['maximum_selected'] }}".replace(':num', e.maximum)
+                }, noResults: function () {
+                    return "{{ $lang['no_results'] }}"
+                }, searching: function () {
+                    return "{{ $lang['searching'] }}"
+                }
+            }
+        }), {define: e.define, require: e.require}
+    }
+</script>

+ 2 - 0
resources/views/form/select.blade.php

@@ -32,3 +32,5 @@
 
     </div>
 </div>
+
+@include('admin::form.select-script')

+ 3 - 4
src/Admin.php

@@ -13,14 +13,13 @@ use Dcat\Admin\Repositories\EloquentRepository;
 use Dcat\Admin\Support\Composer;
 use Dcat\Admin\Traits\HasAssets;
 use Dcat\Admin\Traits\HasPermissions;
-use Dcat\Admin\Traits\Renderable;
+use Dcat\Admin\Traits\HasHtml;
 use Illuminate\Auth\GuardHelpers;
 use Illuminate\Contracts\Auth\Authenticatable;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Event;
-use Illuminate\Support\Fluent;
 use Illuminate\Support\Str;
 
 /**
@@ -29,7 +28,7 @@ use Illuminate\Support\Str;
 class Admin
 {
     use HasAssets;
-    use Renderable;
+    use HasHtml;
 
     /**
      * 版本号.
@@ -265,7 +264,7 @@ class Admin
     }
 
     /**
-     * @return Fluent
+     * @return \Dcat\Admin\Support\Context
      */
     public static function context()
     {

+ 2 - 2
src/AdminServiceProvider.php

@@ -8,11 +8,11 @@ use Dcat\Admin\Layout\Menu;
 use Dcat\Admin\Layout\Navbar;
 use Dcat\Admin\Layout\SectionManager;
 use Dcat\Admin\Extend\Manager;
+use Dcat\Admin\Support\Context;
 use Dcat\Admin\Support\Setting;
 use Dcat\Admin\Support\WebUploader;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Blade;
-use Illuminate\Support\Fluent;
 use Illuminate\Support\ServiceProvider;
 
 class AdminServiceProvider extends ServiceProvider
@@ -211,7 +211,7 @@ class AdminServiceProvider extends ServiceProvider
         $this->app->singleton('admin.extend', Manager::class);
         $this->app->singleton('admin.navbar', Navbar::class);
         $this->app->singleton('admin.menu', Menu::class);
-        $this->app->singleton('admin.context', Fluent::class);
+        $this->app->singleton('admin.context', Context::class);
         $this->app->singleton('admin.setting', function () {
             return Setting::fromDatabase();
         });

+ 29 - 2
src/Form/Field.php

@@ -218,6 +218,11 @@ class Field implements Renderable
      */
     protected $savingCallbacks = [];
 
+    /**
+     * @var bool
+     */
+    protected $runScript = true;
+
     /**
      * Field constructor.
      *
@@ -1138,6 +1143,7 @@ class Field implements Renderable
             'placeholder' => $this->placeholder(),
             'disabled'    => $this->attributes['disabled'] ?? false,
             'formId'      => $this->getFormElementId(),
+            'selector'    => $this->getElementClassSelector(),
         ]);
     }
 
@@ -1257,9 +1263,30 @@ class Field implements Renderable
 
         $this->callComposing();
 
-        Admin::script($this->script);
+        [$html, $script] = Admin::resolveHtml(
+            view($this->view(), $this->variables()),
+            ['runScript' => $this->runScript]
+        );
+
+        $this->script .= $script;
+
+        $this->withScript();
+
+        return $html;
+    }
+
+    public function runScript(bool $value = true)
+    {
+        $this->runScript = $value;
+
+        return $this;
+    }
 
-        return view($this->view(), $this->variables());
+    protected function withScript()
+    {
+        if ($this->script && $this->runScript) {
+            Admin::script($this->script);
+        }
     }
 
     /**

+ 21 - 202
src/Form/Field/HasMany.php

@@ -2,7 +2,6 @@
 
 namespace Dcat\Admin\Form\Field;
 
-use Dcat\Admin\Admin;
 use Dcat\Admin\Form;
 use Dcat\Admin\Form\Field;
 use Dcat\Admin\Form\NestedForm;
@@ -434,188 +433,6 @@ class HasMany extends Field
         return $forms;
     }
 
-    /**
-     * Setup script for this field in different view mode.
-     *
-     * @param string $script
-     *
-     * @return void
-     */
-    protected function setupScript($script)
-    {
-        $method = 'setupScriptFor'.ucfirst($this->viewMode).'View';
-
-        call_user_func([$this, $method], $script);
-    }
-
-    /**
-     * Setup default template script.
-     *
-     * @param string $templateScript
-     *
-     * @return void
-     */
-    protected function setupScriptForDefaultView($templateScript)
-    {
-        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
-
-        /**
-         * When add a new sub form, replace all element key in new sub form.
-         *
-         * @example comments[new___key__][title]  => comments[new_{index}][title]
-         *
-         * {count} is increment number of current sub form count.
-         */
-        $script = <<<JS
-(function () {
-    var nestedIndex = 0;
-    
-    {$this->makeReplaceNestedIndexScript()}
-    
-$('{$this->getContainerElementSelector()}').on('click', '.add', function () {
-
-    var tpl = $('template.{$this->column}-tpl');
-
-    nestedIndex++;
-
-    var template = replaceNestedFormIndex(tpl.html());
-    $('.has-many-{$this->column}-forms').append(template);
-    {$templateScript}
-});
-
-$('{$this->getContainerElementSelector()}').on('click', '.remove', function () {
-    $(this).closest('.has-many-{$this->column}-form').hide();
-    $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
-});
-})()
-JS;
-
-        Admin::script($script);
-    }
-
-    /**
-     * Setup tab template script.
-     *
-     * @param string $templateScript
-     *
-     * @return void
-     */
-    protected function setupScriptForTabView($templateScript)
-    {
-        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
-
-        $script = <<<JS
-(function () {
-    $('{$this->getContainerElementSelector()} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
-        var \$navTab = $(this).siblings('a');
-        var \$pane = $(\$navTab.attr('href'));
-        if( \$pane.hasClass('new') ){
-            \$pane.remove();
-        }else{
-            \$pane.removeClass('active').find('.$removeClass').val(1);
-        }
-        if(\$navTab.closest('li').hasClass('active')){
-            \$navTab.closest('li').remove();
-            $('{$this->getContainerElementSelector()} > .nav > li:nth-child(1) > a').click();
-        }else{
-            \$navTab.closest('li').remove();
-        }
-    });
-        
-    {$this->makeReplaceNestedIndexScript()}
-    
-    var nestedIndex = 0;
-    $('{$this->getContainerElementSelector()} > .header').off('click', '.add').on('click', '.add', function(){
-        nestedIndex++;
-        var navTabHtml = replaceNestedFormIndex($('{$this->getContainerElementSelector()} > template.nav-tab-tpl').html());
-        var paneHtml = replaceNestedFormIndex($('{$this->getContainerElementSelector()} > template.pane-tpl').html());
-        $('{$this->getContainerElementSelector()} > .nav').append(navTabHtml);
-        $('{$this->getContainerElementSelector()} > .tab-content').append(paneHtml);
-        $('{$this->getContainerElementSelector()} > .nav > li:last-child a').click();
-        {$templateScript}
-    });
-    
-    if ($('.has-error').length) {
-        $('.has-error').parent('.tab-pane').each(function () {
-            var tabId = '#'+$(this).attr('id');
-            $('li a[href="'+tabId+'"] i').removeClass('d-none');
-        });
-        
-        var first = $('.has-error:first').parent().attr('id');
-        $('li a[href="#'+first+'"]').tab('show');
-    }
-})();
-JS;
-
-        Admin::script($script);
-    }
-
-    /**
-     * Setup default template script.
-     *
-     * @param string $templateScript
-     *
-     * @return void
-     */
-    protected function setupScriptForTableView($templateScript)
-    {
-        $removeClass = NestedForm::REMOVE_FLAG_CLASS;
-
-        /**
-         * When add a new sub form, replace all element key in new sub form.
-         *
-         * @example comments[new___key__][title]  => comments[new_{index}][title]
-         *
-         * {count} is increment number of current sub form count.
-         */
-        $script = <<<JS
-(function () {
-    var nestedIndex = 0;
-    
-    {$this->makeReplaceNestedIndexScript()}
-    
-    $('{$this->getContainerElementSelector()}').on('click', '.add', function () {
-        var tpl = $('template.{$this->column}-tpl');
-    
-        nestedIndex++;
-
-        var template = replaceNestedFormIndex(tpl.html());
-        $('.has-many-{$this->column}-forms').append(template);
-        {$templateScript}
-    });
-    
-    $('{$this->getContainerElementSelector()}').on('click', '.remove', function () {
-        $(this).closest('.has-many-{$this->column}-form').hide();
-        $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
-    });
-})();
-JS;
-
-        Admin::script($script);
-    }
-
-    /**
-     * @return string
-     */
-    protected function getContainerElementSelector()
-    {
-        return ".has-many-{$this->column}";
-    }
-
-    /**
-     * @return string
-     */
-    protected function makeReplaceNestedIndexScript()
-    {
-        $defaultKey = NestedForm::DEFAULT_KEY_NAME;
-
-        return <<<JS
-function replaceNestedFormIndex(value) {
-    return String(value).replace(/{$defaultKey}/g, nestedIndex);
-}
-JS;
-    }
-
     /**
      * Disable create button.
      *
@@ -663,14 +480,15 @@ JS;
         [$template, $script] = $this->buildNestedForm()
             ->getTemplateHtmlAndScript();
 
-        $this->setupScript($script);
-
-        return parent::render()->with([
-            'forms'        => $this->buildRelatedForms(),
-            'template'     => $template,
-            'relationName' => $this->relationName,
-            'options'      => $this->options,
+        $this->addVariables([
+            'forms'          => $this->buildRelatedForms(),
+            'template'       => $template,
+            'relationName'   => $this->relationName,
+            'options'        => $this->options,
+            'templateScript' => $script,
         ]);
+
+        return parent::render();
     }
 
     /**
@@ -689,6 +507,8 @@ JS;
 
         /* @var Field $field */
         foreach ($this->buildNestedForm()->fields() as $field) {
+            $field->runScript(false);
+
             if (is_a($field, Hidden::class)) {
                 $hidden[] = $field->render();
             } else {
@@ -702,8 +522,8 @@ JS;
             /*
              * Get and remove the last script of Admin::$script stack.
              */
-            if ($field->getScript()) {
-                $scripts[] = array_pop(Admin::asset()->script);
+            if ($script = $field->getScript()) {
+                $scripts[] = $script;
             }
         }
 
@@ -717,19 +537,18 @@ JS;
         /* Build cell with hidden elements */
         $template .= '<td class="hidden">'.implode('', $hidden).'</td>';
 
-        $this->setupScript(implode(";\r\n", $scripts));
-
         // specify a view to render.
         $this->view = $this->views[$this->viewMode];
 
-        Admin::style('.table-has-many .input-group{flex-wrap: nowrap!important}');
-
-        return parent::render()->with([
-            'headers'      => $headers,
-            'forms'        => $this->buildRelatedForms(),
-            'template'     => $template,
-            'relationName' => $this->relationName,
-            'options'      => $this->options,
+        $this->addVariables([
+            'headers'        => $headers,
+            'forms'          => $this->buildRelatedForms(),
+            'template'       => $template,
+            'relationName'   => $this->relationName,
+            'options'        => $this->options,
+            'templateScript' => implode(";\r\n", $scripts),
         ]);
+
+        return parent::render();
     }
 }

+ 46 - 204
src/Form/Field/Select.php

@@ -13,9 +13,6 @@ class Select extends Field
 {
     use CanCascadeFields;
 
-    public static $js = '@select2';
-    public static $css = '@select2';
-
     protected $cascadeEvent = 'change';
 
     /**
@@ -106,33 +103,9 @@ class Select extends Field
             $class = static::FIELD_CLASS_PREFIX.$field;
         }
 
-        $sourceUrl = admin_url($sourceUrl);
-
-        $script = <<<JS
-$(document).off('change', "{$this->getElementClassSelector()}");
-$(document).on('change', "{$this->getElementClassSelector()}", function () {
-    var target = $(this).closest('.fields-group').find(".$class");
-    
-    if (String(this.value) !== '0' && ! this.value) {
-        return;
-    }
-    $.ajax("$sourceUrl?q="+this.value).then(function (data) {
-        target.find("option").remove();
-        $(target).select2({
-            data: $.map(data, function (d) {
-                d.id = d.$idField;
-                d.text = d.$textField;
-                return d;
-            })
-        }).val(target.attr('data-value').split(',')).trigger('change');
-    });
-});
-$("{$this->getElementClassSelector()}").trigger('change');
-JS;
-
-        Admin::script($script);
+        $url = admin_url($sourceUrl);
 
-        return $this;
+        return $this->addVariables(['load' => compact('url', 'class', 'idField', 'textField')]);
     }
 
     /**
@@ -158,47 +131,12 @@ JS;
             return admin_url($url);
         }, (array) $sourceUrls));
 
-        $script = <<<JS
-(function () {
-    var fields = '$fieldsStr'.split('^');
-    var urls = '$urlsStr'.split('^');
-    
-    var refreshOptions = function(url, target) {
-        $.ajax(url).then(function(data) {
-            target.find("option").remove();
-            $(target).select2({
-                data: $.map(data, function (d) {
-                    d.id = d.$idField;
-                    d.text = d.$textField;
-                    return d;
-                })
-            }).val(target.data('value').split(',')).trigger('change');
-        });
-    };
-    
-    $(document).off('change', "{$this->getElementClassSelector()}");
-    $(document).on('change', "{$this->getElementClassSelector()}", function () {
-        var _this = this;
-        var promises = [];
-
-        fields.forEach(function(field, index){
-            var target = $(_this).closest('.fields-group').find('.' + fields[index]);
-
-            if (_this.value !== '0' && ! _this.value) {
-                return;
-            }
-            promises.push(refreshOptions(urls[index] + "?q="+ _this.value, target));
-        });
-    
-        $.when(promises).then(function() {});
-    });
-    $("{$this->getElementClassSelector()}").trigger('change');
-})()
-JS;
-
-        Admin::script($script);
-
-        return $this;
+        return $this->addVariables(['loads' => [
+            'fields'    => $fieldsStr,
+            'urls'      => $urlsStr,
+            'idField'   => $idField,
+            'textField' => $textField,
+        ]]);
     }
 
     /**
@@ -255,39 +193,31 @@ JS;
         $ajaxOptions = [
             'url' => admin_url($url.'?'.http_build_query($parameters)),
         ];
-        $configs = array_merge([
-            'allowClear'  => true,
-            'placeholder' => [
-                'id'   => '',
-                'text' => $this->placeholder(),
-            ],
-        ], $this->config);
-
-        $configs = json_encode($configs);
-        $configs = substr($configs, 1, strlen($configs) - 2);
 
-        $ajaxOptions = json_encode(array_merge($ajaxOptions, $options));
+        $ajaxOptions = array_merge($ajaxOptions, $options);
 
-        $this->script = <<<JS
-$.ajax({$ajaxOptions}).done(function(data) {
+        return $this->addVariables(['remoteOptions' => $ajaxOptions]);
+    }
 
-  $("{$this->getElementClassSelector()}").each(function (_, select) {
-      select = $(select);
+    /**
+     * @param string|array $key
+     * @param mixed        $value
+     *
+     * @return $this
+     */
+    public function addDefaultConfig($key, $value = null)
+    {
+        if (is_array($key)) {
+            foreach ($key as $k => $v) {
+                $this->addDefaultConfig($k, $v);
+            }
 
-      select.select2({
-        data: data,
-        $configs
-      });
-      
-      var value = select.data('value') + '';
-      
-      if (value) {
-        select.val(value.split(',')).trigger("change")
-      }
-  });
-});
+            return $this;
+        }
 
-JS;
+        if (! isset($this->config[$key])) {
+            $this->config[$key] = $value;
+        }
 
         return $this;
     }
@@ -303,55 +233,13 @@ JS;
      */
     public function ajax(string $url, string $idField = 'id', string $textField = 'text')
     {
-        $configs = array_merge([
-            'allowClear'         => true,
-            'placeholder'        => $this->placeholder(),
+        $this->addDefaultConfig([
             'minimumInputLength' => 1,
-        ], $this->config);
-
-        $configs = json_encode($configs);
-        $configs = substr($configs, 1, strlen($configs) - 2);
+        ]);
 
         $url = admin_url($url);
 
-        $this->script = <<<JS
-
-$("{$this->getElementClassSelector()}").select2({
-  ajax: {
-    url: "$url",
-    dataType: 'json',
-    delay: 250,
-    data: function (params) {
-      return {
-        q: params.term,
-        page: params.page
-      };
-    },
-    processResults: function (data, params) {
-      params.page = params.page || 1;
-
-      return {
-        results: $.map(data.data, function (d) {
-                   d.id = d.$idField;
-                   d.text = d.$textField;
-                   return d;
-                }),
-        pagination: {
-          more: data.next_page_url
-        }
-      };
-    },
-    cache: true
-  },
-  $configs,
-  escapeMarkup: function (markup) {
-      return markup;
-  }
-});
-
-JS;
-
-        return $this;
+        return $this->addVariables(['ajax' => compact('url', 'idField', 'textField')]);
     }
 
     /**
@@ -386,35 +274,22 @@ JS;
      */
     public function render()
     {
-        static::defineLang();
-
-        $configs = array_merge([
+        $this->addDefaultConfig([
             'allowClear'  => true,
             'placeholder' => [
                 'id'   => '',
                 'text' => $this->placeholder(),
             ],
-        ], $this->config);
-
-        $configs = json_encode($configs);
-
-        if (empty($this->script)) {
-            $this->script = "$(\"{$this->getElementClassSelector()}\").select2($configs);";
-        }
+        ]);
 
         $this->addCascadeScript();
 
-        if ($this->options instanceof \Closure) {
-            $this->options = $this->options->bindTo($this->values());
-
-            $this->options(call_user_func($this->options, $this->value(), $this));
-        }
-
-        $this->options = array_filter($this->options, 'strlen');
+        $this->formatOptions();
 
         $this->addVariables([
             'options' => $this->options,
             'groups'  => $this->groups,
+            'configs' => $this->config,
         ]);
 
         $this->attribute('data-value', implode(',', Helper::array($this->value())));
@@ -422,6 +297,17 @@ JS;
         return parent::render();
     }
 
+    protected function formatOptions()
+    {
+        if ($this->options instanceof \Closure) {
+            $this->options = $this->options->bindTo($this->values());
+
+            $this->options(call_user_func($this->options, $this->value(), $this));
+        }
+
+        $this->options = array_filter($this->options, 'strlen');
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -435,48 +321,4 @@ JS;
 
         return $this;
     }
-
-    /**
-     * @return void
-     */
-    public static function defineLang()
-    {
-        $lang = trans('select2');
-        if (! is_array($lang) || empty($lang)) {
-            return;
-        }
-
-        $locale = config('app.locale');
-
-        Admin::script(
-            <<<JS
-(function () {
-    if (! $.fn.select2) {
-        return;
-    }
-    var e = $.fn.select2.amd;
-
-    return e.define("select2/i18n/{$locale}", [], function () {
-        return {
-            errorLoading: function () {
-                return "{$lang['error_loading']}"
-            }, inputTooLong: function (e) {
-                return "{$lang['input_too_long']}".replace(':num', e.input.length - e.maximum)
-            }, inputTooShort: function (e) {
-                return "{$lang['input_too_short']}".replace(':num', e.minimum - e.input.length)
-            }, loadingMore: function () {
-                return "{$lang['loading_more']}"
-            }, maximumSelected: function (e) {
-                return "{$lang['maximum_selected']}".replace(':num', e.maximum)
-            }, noResults: function () {
-                return "{$lang['no_results']}"
-            }, searching: function () {
-                 return "{$lang['searching']}"
-            }
-        }
-    }), {define: e.define, require: e.require}
-})()
-JS
-        );
-    }
 }

+ 3 - 3
src/Form/NestedForm.php

@@ -379,13 +379,13 @@ class NestedForm
         foreach ($this->fields() as $field) {
 
             //when field render, will push $script to Admin
-            $html .= $field->render();
+            $html .= $field->runScript(false)->render();
 
             /*
              * Get and remove the last script of Admin::$script stack.
              */
-            if ($field->getScript()) {
-                $scripts[] = array_pop(Admin::asset()->script);
+            if ($script = $field->getScript()) {
+                $scripts[] = $script;
             }
         }
 

+ 34 - 0
src/Support/Context.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Dcat\Admin\Support;
+
+use Dcat\EasyExcel\Support\Arr;
+use Illuminate\Support\Fluent;
+
+/**
+ * Class Context
+ *
+ * @property string $favicon
+ * @property string $metaTitle
+ * @property string $pjaxContainerId
+ * @property array|null $html
+ * @property array|null $ignoreQueries
+ * @property array|null $jsVariables
+ */
+class Context extends Fluent
+{
+    public function get($key, $default = null)
+    {
+        return Arr::get($this->attributes, $key, $default);
+    }
+
+    public function forget($keys)
+    {
+        Arr::forget($this->attributes, $keys);
+    }
+
+    public function flush()
+    {
+        $this->attributes = [];
+    }
+}

+ 0 - 2
src/Traits/HasAssets.php

@@ -2,8 +2,6 @@
 
 namespace Dcat\Admin\Traits;
 
-use Dcat\Admin\Support\Helper;
-
 trait HasAssets
 {
     /**

+ 11 - 9
src/Traits/HasBuilderEvents.php

@@ -2,6 +2,8 @@
 
 namespace Dcat\Admin\Traits;
 
+use Dcat\Admin\Admin;
+
 trait HasBuilderEvents
 {
     public static function resolving(callable $callback, bool $once = false)
@@ -26,11 +28,11 @@ trait HasBuilderEvents
 
     protected function fireBuilderEvent($key, ...$params)
     {
-        $storage = app('admin.context');
+        $context = Admin::context();
 
-        $key = static::formatBuilderEventKey($key);
+        $key = static::formatEventKey($key);
 
-        $listeners = $storage->get($key) ?: [];
+        $listeners = $context->get($key) ?: [];
 
         foreach ($listeners as $k => $listener) {
             [$callback, $once] = $listener;
@@ -42,23 +44,23 @@ trait HasBuilderEvents
             call_user_func($callback, $this, ...$params);
         }
 
-        $storage[$key] = $listeners;
+        $context[$key] = $listeners;
     }
 
     protected static function addBuilderListeners($key, $callback, $once)
     {
-        $storage = app('admin.context');
+        $context = Admin::context();
 
-        $key = static::formatBuilderEventKey($key);
+        $key = static::formatEventKey($key);
 
-        $listeners = $storage->get($key) ?: [];
+        $listeners = $context->get($key) ?: [];
 
         $listeners[] = [$callback, $once];
 
-        $storage[$key] = $listeners;
+        $context[$key] = $listeners;
     }
 
-    protected static function formatBuilderEventKey($key)
+    protected static function formatEventKey($key)
     {
         return static::class.':'.$key;
     }

+ 33 - 14
src/Traits/Renderable.php → src/Traits/HasHtml.php

@@ -6,7 +6,7 @@ use Dcat\Admin\Support\Helper;
 use DOMElement;
 use DOMDocument;
 
-trait Renderable
+trait HasHtml
 {
     protected static $shouldResolveTags = ['style', 'script', 'template'];
 
@@ -39,25 +39,38 @@ trait Renderable
      */
     public static function view(string $view, array $data = [])
     {
-        return static::render(view($view, $data));
+        return static::resolveHtml(view($view, $data))[0];
     }
 
     /**
-     * @param string|\Illuminate\Contracts\Support\Renderable $view
+     * @param string|\Illuminate\Contracts\Support\Renderable $content
      * @param array                                           $data
+     * @param array                                           $options
      *
      * @throws \Throwable
      *
-     * @return string
+     * @return array [$html, $script]
      */
-    public static function render($value): string
+    public static function resolveHtml($content, array $options = []): array
     {
-        $dom = static::getDOMDocument(Helper::render($value));
+        $dom = static::getDOMDocument(Helper::render($content));
 
         $head = $dom->getElementsByTagName('head')->item(0) ?: null;
         $body = $dom->getElementsByTagName('body')->item(0) ?: null;
 
-        return static::resolveElement($head).static::resolveElement($body);
+        [$headHtml, $headScript] = static::resolveElement($head);
+        [$bodyHtml, $bodyScript] = static::resolveElement($body);
+
+        $script = $headScript.$bodyScript;
+
+        $runScript = $options['runScript'] ?? true;
+        if ($runScript) {
+            static::script($script);
+
+            $script = '';
+        }
+
+        return [$headHtml.$bodyHtml, $script];
     }
 
     /**
@@ -83,7 +96,7 @@ trait Renderable
     /**
      * @param DOMElement $element
      *
-     * @return void
+     * @return void|string
      */
     protected static function resolve(DOMElement $element)
     {
@@ -95,7 +108,7 @@ trait Renderable
     /**
      * @param DOMElement $element
      *
-     * @return void
+     * @return string|void
      */
     protected static function resolveScript(DOMElement $element)
     {
@@ -110,7 +123,13 @@ trait Renderable
                 static::asset()->collect($require);
             }
 
-            static::script('(function () {'.$script.'})()');
+            $script = '(function () {'.$script.'})();';
+
+            if ($element->hasAttribute('once')) {
+                return static::script($script);
+            }
+
+            return $script;
         }
     }
 
@@ -143,10 +162,10 @@ trait Renderable
 
     protected static function resolveElement(?DOMElement $element)
     {
-        $html = '';
+        $html = $script = '';
 
         if (! $element) {
-            return $html;
+            return [$html, $script];
         }
 
         foreach ($element->childNodes as $child) {
@@ -154,7 +173,7 @@ trait Renderable
                 $child instanceof DOMElement
                 && in_array($child->tagName, static::$shouldResolveTags, true)
             ) {
-                static::resolve($child);
+                $script .= static::resolve($child);
 
                 continue;
             }
@@ -162,6 +181,6 @@ trait Renderable
             $html .= trim($element->ownerDocument->saveHTML($child));
         }
 
-        return $html;
+        return [$html, $script];
     }
 }