Przeglądaj źródła

引入jquery.initialize插件

jqh 5 lat temu
rodzic
commit
92fc19b4aa

+ 34 - 0
resources/assets/dcat/js/Dcat.js

@@ -7,6 +7,7 @@ let $ = jQuery,
     pjaxResponded = false,
     bootingCallbacks = [],
     actions = {},
+    initialized = {},
     defaultOptions = {
         pjax_container_selector: '#pjax-container',
     };
@@ -96,6 +97,39 @@ export default class Dcat {
         _window.Dcat.ready(run);
     }
 
+    /**
+     * 监听动态生成元素.
+     *
+     * @param selector
+     * @param callback
+     * @param options
+     */
+    initialize(selector, callback, options) {
+        if (initialized[selector]) {
+            initialized[selector].takeRecords();
+            initialized[selector].disconnect();
+        }
+
+        let self = this;
+
+        // 这里必须使用定时器,否则无法立即停止上次绑定的观察回调
+        setTimeout(function () {
+            initialized[selector] = $.initialize(selector, function () {
+                var $this = $(this);
+                if ($this.hasClass('initialized')) {
+                    return;
+                }
+                $this.addClass('initialized');
+
+                // 生成随机ID
+                var id = "_"+self.helpers.random();
+                $this.attr('id', id);
+
+                callback.call(this, $(this), id)
+            }, options);
+        }, 1)
+    }
+
     /**
      * 主动触发 ready 事件
      */

+ 22 - 0
resources/assets/dcat/plugins/jquery.initialize/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2016 Adam Pietrasiak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 157 - 0
resources/assets/dcat/plugins/jquery.initialize/jquery.initialize.js

@@ -0,0 +1,157 @@
+/*!
+ * https://github.com/adampietrasiak/jquery.initialize
+ *
+ * Copyright (c) 2015-2016 Adam Pietrasiak
+ * Released under the MIT license
+ * https://github.com/timpler/jquery.initialize/blob/master/LICENSE
+ *
+ * This is based on MutationObserver
+ * https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
+ */
+;(function ($) {
+
+    "use strict";
+
+    var combinators = [' ', '>', '+', '~']; // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors#Combinators
+    var fraternisers = ['+', '~']; // These combinators involve siblings.
+    var complexTypes = ['ATTR', 'PSEUDO', 'ID', 'CLASS']; // These selectors are based upon attributes.
+
+    //Check if browser supports "matches" function
+    if (!Element.prototype.matches) {
+        Element.prototype.matches = Element.prototype.matchesSelector ||
+            Element.prototype.webkitMatchesSelector ||
+            Element.prototype.mozMatchesSelector ||
+            Element.prototype.msMatchesSelector;
+    }
+
+    // Understand what kind of selector the initializer is based upon.
+    function grok(msobserver) {
+        if (!$.find.tokenize) {
+            // This is an old version of jQuery, so cannot parse the selector.
+            // Therefore we must assume the worst case scenario. That is, that
+            // this is a complicated selector. This feature was available in:
+            // https://github.com/jquery/sizzle/issues/242
+            msobserver.isCombinatorial = true;
+            msobserver.isFraternal = true;
+            msobserver.isComplex = true;
+            return;
+        }
+
+        // Parse the selector.
+        msobserver.isCombinatorial = false;
+        msobserver.isFraternal = false;
+        msobserver.isComplex = false;
+        var token = $.find.tokenize(msobserver.selector);
+        for (var i = 0; i < token.length; i++) {
+            for (var j = 0; j < token[i].length; j++) {
+                if (combinators.indexOf(token[i][j].type) != -1)
+                    msobserver.isCombinatorial = true; // This selector uses combinators.
+
+                if (fraternisers.indexOf(token[i][j].type) != -1)
+                    msobserver.isFraternal = true; // This selector uses sibling combinators.
+
+                if (complexTypes.indexOf(token[i][j].type) != -1)
+                    msobserver.isComplex = true; // This selector is based on attributes.
+            }
+        }
+    }
+
+    // MutationSelectorObserver represents a selector and it's associated initialization callback.
+    var MutationSelectorObserver = function (selector, callback, options) {
+        this.selector = selector.trim();
+        this.callback = callback;
+        this.options = options;
+
+        grok(this);
+    };
+
+    // List of MutationSelectorObservers.
+    var msobservers = [];
+    msobservers.initialize = function (selector, callback, options) {
+
+        // Wrap the callback so that we can ensure that it is only
+        // called once per element.
+        var seen = [];
+        var callbackOnce = function () {
+            if (seen.indexOf(this) == -1) {
+                seen.push(this);
+                $(this).each(callback);
+            }
+        };
+
+        // See if the selector matches any elements already on the page.
+        $(options.target).find(selector).each(callbackOnce);
+
+        // Then, add it to the list of selector observers.
+        var msobserver = new MutationSelectorObserver(selector, callbackOnce, options)
+        this.push(msobserver);
+
+        // The MutationObserver watches for when new elements are added to the DOM.
+        var observer = new MutationObserver(function (mutations) {
+            var matches = [];
+
+            // For each mutation.
+            for (var m = 0; m < mutations.length; m++) {
+
+                // If this is an attributes mutation, then the target is the node upon which the mutation occurred.
+                if (mutations[m].type == 'attributes') {
+                    // Check if the mutated node matchs.
+                    if (mutations[m].target.matches(msobserver.selector))
+                        matches.push(mutations[m].target);
+
+                    // If the selector is fraternal, query siblings of the mutated node for matches.
+                    if (msobserver.isFraternal)
+                        matches.push.apply(matches, mutations[m].target.parentElement.querySelectorAll(msobserver.selector));
+                    else
+                        matches.push.apply(matches, mutations[m].target.querySelectorAll(msobserver.selector));
+                }
+
+                // If this is an childList mutation, then inspect added nodes.
+                if (mutations[m].type == 'childList') {
+                    // Search added nodes for matching selectors.
+                    for (var n = 0; n < mutations[m].addedNodes.length; n++) {
+                        if (!(mutations[m].addedNodes[n] instanceof Element)) continue;
+
+                        // Check if the added node matches the selector
+                        if (mutations[m].addedNodes[n].matches(msobserver.selector))
+                            matches.push(mutations[m].addedNodes[n]);
+
+                        // If the selector is fraternal, query siblings for matches.
+                        if (msobserver.isFraternal)
+                            matches.push.apply(matches, mutations[m].addedNodes[n].parentElement.querySelectorAll(msobserver.selector));
+                        else
+                            matches.push.apply(matches, mutations[m].addedNodes[n].querySelectorAll(msobserver.selector));
+                    }
+                }
+            }
+
+            // For each match, call the callback using jQuery.each() to initialize the element (once only.)
+            for (var i = 0; i < matches.length; i++) {
+                $(matches[i]).each(msobserver.callback);
+            }
+        });
+
+        // Observe the target element.
+        var defaultObeserverOpts = { childList: true, subtree: true, attributes: msobserver.isComplex };
+        observer.observe(options.target, options.observer || defaultObeserverOpts );
+
+        return observer;
+    };
+
+    // Deprecated API (does not work with jQuery >= 3.1.1):
+    $.fn.initialize = function (callback, options) {
+        return msobservers.initialize(this.selector, callback, $.extend({}, $.initialize.defaults, options));
+    };
+
+    // Supported API
+    $.initialize = function (selector, callback, options) {
+        return msobservers.initialize(selector, callback, $.extend({}, $.initialize.defaults, options));
+    };
+
+    // Options
+    $.initialize.defaults = {
+        target: document.documentElement, // Defaults to observe the entire document.
+        observer: null // MutationObserverInit: Defaults to internal configuration if not provided.
+    }
+
+})(jQuery);

Plik diff jest za duży
+ 0 - 0
resources/assets/dcat/plugins/jquery.initialize/jquery.initialize.min.js


+ 2 - 20
resources/views/form/container.blade.php

@@ -4,30 +4,12 @@
         <div class="pull-right">{!! $form->renderTools() !!}</div>
     </div>
 @endif
-<div class="box-body" {!! $tabObj->isEmpty() && !$form->hasRows() ? 'style="margin-top: 10px"' : '' !!} >
+<div class="box-body" {!! $tabObj->isEmpty() && !$form->hasRows() ? 'style="margin-top: 6px"' : '' !!} >
     @if(!$tabObj->isEmpty())
         @include('admin::form.tab', compact('tabObj', 'form'))
     @else
         <div class="fields-group">
-            @if($form->hasRows())
-                <div class="ml-2 mb-2">
-                    @foreach($form->rows() as $row)
-                        {!! $row->render() !!}
-                    @endforeach
-
-                    @foreach($form->fields() as $field)
-                        @if($field instanceof \Dcat\Admin\Form\Field\Hidden)
-                            {!! $field->render() !!}
-                        @endif
-                    @endforeach
-                </div>
-            @elseif($form->layout()->hasColumns())
-                {!! $form->layout()->build() !!}
-            @else
-                @foreach($form->fields() as $field)
-                    {!! $field->render() !!}
-                @endforeach
-            @endif
+            @include('admin::form.fields', ['rows' => $form->rows(), 'fields' => $form->fields(), 'layout' => $form->layout()])
         </div>
     @endif
 </div>

+ 6 - 6
resources/views/form/editor.blade.php

@@ -1,22 +1,22 @@
 <div class="{{$viewClass['form-group']}}">
 
-    <label for="{{$id}}" class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
+    <label class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
 
     <div class="{{$viewClass['field']}}">
 
         @include('admin::form.error')
 
-        <textarea class="form-control {{$class}}" id="{{$id}}" name="{{$name}}" placeholder="{{ $placeholder }}" {!! $attributes !!} >{{ $value }}</textarea>
+        <textarea class="form-control {{$class}} form-field-editor" name="{{$name}}" placeholder="{{ $placeholder }}" {!! $attributes !!} >{{ $value }}</textarea>
 
         @include('admin::form.help-block')
 
     </div>
 </div>
 
-<script require="@tinymce">
-    var opts = {!! $options !!};
+<script require="@tinymce" init=".form-field-editor" id>
+    var opts = {!! admin_javascript_json($options) !!};
 
-    opts.selector = replaceNestedFormIndex(opts.selector);
+    opts.selector = '#'+id;
 
     if (! opts.init_instance_callback) {
         opts.init_instance_callback = function (editor) {
@@ -27,7 +27,7 @@
                     content = content.length && content.join('');
                 }
 
-                $(replaceNestedFormIndex('#{{ $id }}')).val(String(content).replace('<p><br data-mce-bogus="1"></p>', '').replace('<p><br></p>', ''));
+                $this.val(String(content).replace('<p><br data-mce-bogus="1"></p>', '').replace('<p><br></p>', ''));
             });
         }
     }

+ 19 - 0
resources/views/form/fields.blade.php

@@ -0,0 +1,19 @@
+@if($rows)
+    <div class="ml-2 mb-2" style="margin-top: -0.5rem">
+        @foreach($rows as $row)
+            {!! $row->render() !!}
+        @endforeach
+
+        @foreach($fields as $field)
+            @if($field instanceof Dcat\Admin\Form\Field\Hidden)
+                {!! $field->render() !!}
+            @endif
+        @endforeach
+    </div>
+@elseif($layout->hasColumns())
+    {!! $layout->build() !!}
+@else
+    @foreach($fields as $field)
+        {!! $field->render() !!}
+    @endforeach
+@endif

+ 9 - 11
resources/views/form/file.blade.php

@@ -13,7 +13,7 @@
     }
 </style>
 
-<div id="{{ $id }}-container" class="{{$viewClass['form-group']}}">
+<div class="{{$viewClass['form-group']}} form-field-file">
 
     <label for="{{$column}}" class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
 
@@ -21,7 +21,7 @@
 
         @include('admin::form.error')
 
-        <input name="{{ $name }}" id="{{ $id }}" type="hidden" />
+        <input name="{{ $name }}" class="file-input" type="hidden" />
 
         <div class="web-uploader {{ $fileType }}">
             <div class="queueList">
@@ -49,30 +49,28 @@
     </div>
 </div>
 
-<script require="@webuploader">
+<script require="@webuploader" init=".form-field-file">
     var uploader,
         newPage,
-        cID = replaceNestedFormIndex('#{{ $id }}-container'),
-        ID = replaceNestedFormIndex('#{{ $id }}'),
         options = {!! $options !!};
 
     init();
 
     function init() {
         var opts = $.extend({
-            selector: cID,
-            addFileButton: cID+' .add-file-button',
-            inputSelector: ID,
+            selector: $this,
+            addFileButton: $this.find('.add-file-button'),
+            inputSelector: $this.find('.file-input'),
         }, options);
 
         opts.upload = $.extend({
             pick: {
-                id: cID+' .file-picker',
+                id: $this.find('.file-picker'),
                 name: '_file_',
                 label: '<i class="feather icon-folder"></i>&nbsp; {!! trans('admin.uploader.add_new_media') !!}'
             },
-            dnd: cID+' .dnd-area',
-            paste: cID+' .web-uploader'
+            dnd: $this.find('.dnd-area'),
+            paste: $this.find('.web-uploader')
         }, opts);
 
         uploader = Dcat.Uploader(opts);

+ 4 - 4
resources/views/form/markdown.blade.php

@@ -4,13 +4,13 @@
 
 <div class="{{$viewClass['form-group']}}">
 
-    <label for="{{$id}}" class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
+    <label class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
 
     <div class="{{$viewClass['field']}}">
 
         @include('admin::form.error')
 
-        <div id="{{$id}}" class="{{$class}}" {!! $attributes !!}>
+        <div class="{{$class}} form-field-markdown" {!! $attributes !!}>
             <textarea class="d-none" name="{{$name}}" placeholder="{{ $placeholder }}">{!! $value !!}</textarea>
         </div>
 
@@ -19,6 +19,6 @@
     </div>
 </div>
 
-<script require="@markdown">
-    editormd(replaceNestedFormIndex("{{ $id }}"), {!! $options !!});
+<script require="@markdown" init=".form-field-markdown" id>
+    editormd(id, {!! $options !!});
 </script>

+ 6 - 8
resources/views/form/tree.blade.php

@@ -2,7 +2,7 @@
 
     <label class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
 
-    <div id="{{ $id }}" class="{{$viewClass['field']}}">
+    <div class="{{$viewClass['field']}} form-field-tree">
 
         @include('admin::form.error')
 
@@ -22,21 +22,19 @@
     </div>
 </div>
 
-<script require="@jstree">
-    var selector = replaceNestedFormIndex('#{{ $id }}'),
-        tree = selector+' .jstree-wrapper .da-tree',
-        $tree = $(tree),
-        $input = $(selector+' .hidden-input'),
+<script require="@jstree" init=".form-field-tree">
+    var $tree = $this.find('.jstree-wrapper .da-tree'),
+        $input = $this.find('.hidden-input'),
         opts = {!! admin_javascript_json($options) !!},
         parents = {!! json_encode($parents) !!};
 
     opts.core = opts.core || {};
     opts.core.data = {!! json_encode($nodes) !!};
 
-    $(document).on("click", selector+" input[value=1]", function () {
+    $this.find('input[value=1]').on("click", function () {
         $(this).parents('.jstree-wrapper').find('.da-tree').jstree($(this).prop("checked") ? "check_all" : "uncheck_all");
     });
-    $(document).on("click", selector+" input[value=2]", function () {
+    $this.find('input[value=2]').on("click", function () {
         $(this).parents('.jstree-wrapper').find('.da-tree').jstree($(this).prop("checked") ? "open_all" : "close_all");
     });
 

+ 1 - 19
resources/views/widgets/form.blade.php

@@ -9,25 +9,7 @@
                 @endif
             @endforeach
         @else
-            @if($rows)
-                <div class="ml-2 mb-2">
-                    @foreach($rows as $row)
-                        {!! $row->render() !!}
-                    @endforeach
-
-                    @foreach($fields as $field)
-                        @if($field instanceof \Dcat\Admin\Form\Field\Hidden)
-                            {!! $field->render() !!}
-                        @endif
-                    @endforeach
-                </div>
-            @elseif($layout->hasColumns())
-                {!! $layout->build() !!}
-            @else
-                @foreach($fields as $field)
-                    {!! $field->render() !!}
-                @endforeach
-            @endif
+            @include('admin::form.fields')
         @endif
     </div>
 

+ 5 - 0
src/Form/BlockForm.php

@@ -144,6 +144,11 @@ HTML;
         return $this->form->getKey();
     }
 
+    public function model()
+    {
+        return $this->form->model();
+    }
+
     public function __call($method, $arguments)
     {
         try {

+ 37 - 0
src/Form/Concerns/HasLayout.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Dcat\Admin\Form\Concerns;
+
+use Closure;
+use Dcat\Admin\Form\Layout;
+
+trait HasLayout
+{
+    /**
+     * @var Layout
+     */
+    protected $layout;
+
+    /**
+     * @param int|float $width
+     * @param Closure   $callback
+     *
+     * @return $this
+     */
+    public function column($width, Closure $callback)
+    {
+        $this->layout()->onlyColumn($width, function () use ($callback) {
+            $callback($this);
+        });
+
+        return $this;
+    }
+
+    /**
+     * @return Layout
+     */
+    public function layout()
+    {
+        return $this->layout ?: ($this->layout = new Layout($this));
+    }
+}

+ 10 - 0
src/Form/EmbeddedForm.php

@@ -128,6 +128,16 @@ class EmbeddedForm
         return $this;
     }
 
+    public function getKey()
+    {
+        return $this->parent->getKey();
+    }
+
+    public function model()
+    {
+        return $this->parent->model();
+    }
+
     /**
      * Set original values for fields.
      *

+ 2 - 3
src/Form/Field/Editor.php

@@ -108,11 +108,10 @@ class Editor extends Field
     }
 
     /**
-     * @return string
+     * @return array
      */
     protected function formatOptions()
     {
-        $this->options['selector'] = '#'.$this->id;
         $this->options['language'] = config('app.locale');
         $this->options['readonly'] = ! empty($this->attributes['readonly']) || ! empty($this->attributes['disabled']);
 
@@ -120,7 +119,7 @@ class Editor extends Field
             $this->options['images_upload_url'] = $this->defaultImageUploadUrl();
         }
 
-        return JavaScript::format($this->options);
+        return $this->options;
     }
 
     /**

+ 28 - 13
src/Form/NestedForm.php

@@ -2,10 +2,7 @@
 
 namespace Dcat\Admin\Form;
 
-use Dcat\Admin\Admin;
 use Dcat\Admin\Form;
-use Dcat\Admin\Form\Field\MultipleSelectTable;
-use Dcat\Admin\Form\Field\SelectTable;
 use Dcat\Admin\Support\Helper;
 use Dcat\Admin\Widgets\Form as WidgetForm;
 use Illuminate\Support\Arr;
@@ -70,6 +67,11 @@ use Illuminate\Support\Collection;
  */
 class NestedForm
 {
+    use Form\Concerns\HandleCascadeFields;
+    use Form\Concerns\HasRows;
+    use Form\Concerns\HasTabs;
+    use Form\Concerns\HasLayout;
+
     const DEFAULT_KEY_NAME = '__LA_KEY__';
 
     const REMOVE_FLAG_NAME = '_remove_';
@@ -143,11 +145,16 @@ class NestedForm
      *
      * @return Form
      */
-    public function getForm()
+    public function form()
     {
         return $this->form;
     }
 
+    public function model()
+    {
+        return $this->form->model();
+    }
+
     /**
      * Set original values for fields.
      *
@@ -438,15 +445,7 @@ class NestedForm
         return Helper::formatElementName($name ?: $this->relationName);
     }
 
-    /**
-     * Add nested-form fields dynamically.
-     *
-     * @param string $method
-     * @param array  $arguments
-     *
-     * @return mixed
-     */
-    public function __call($method, $arguments)
+    protected function resolveField($method, $arguments)
     {
         if ($className = Form::findFieldClass($method)) {
             $column = Arr::get($arguments, 0, '');
@@ -458,6 +457,22 @@ class NestedForm
 
             $field = $this->formatField($field);
 
+            return $field;
+        }
+    }
+
+    /**
+     * Add nested-form fields dynamically.
+     *
+     * @param string $method
+     * @param array  $arguments
+     *
+     * @return mixed
+     */
+    public function __call($method, $arguments)
+    {
+        if ($field = $this->resolveField($method, $arguments)) {
+
             $this->pushField($field);
 
             return $field;

+ 2 - 0
src/Http/Controllers/UserController.php

@@ -129,6 +129,8 @@ class UserController extends AdminController
 
             $form->display('id', 'ID');
 
+            $form->editor('test');
+
             $form->text('username', trans('admin.username'))
                 ->required()
                 ->creationRules(['required', "unique:{$connection}.{$userTable}"])

+ 6 - 2
src/Layout/Asset.php

@@ -39,6 +39,9 @@ class Asset
             'js'  => '@admin/dcat/plugins/vendors.min.js',
             'css' => '@admin/dcat/plugins/vendors.min.css',
         ],
+        '@jquery.initialize' => [
+            'js' => '@admin/dcat/plugins/jquery.initialize/jquery.initialize.min.js',
+        ],
         '@datatables' => [
             'css' => '@admin/dcat/plugins/tables/datatable/datatables.min.css',
         ],
@@ -240,6 +243,7 @@ class Asset
         'pjax'      => '@pjax',
         'validator' => '@validator',
         'layer'     => '@layer',
+        'init'      => '@jquery.initialize',
     ];
 
     /**
@@ -568,8 +572,8 @@ class Asset
     protected function addFontCss()
     {
         $this->fonts && ($this->baseCss = array_merge(
-                $this->baseCss,
-                (array) $this->fonts
+            $this->baseCss,
+            (array) $this->fonts
         ));
     }
 

+ 1 - 1
src/Traits/HasAuthorization.php

@@ -3,7 +3,7 @@
 namespace Dcat\Admin\Traits;
 
 use Dcat\Admin\Admin;
-use Dcat\Admin\Models\HasPermissions;
+use Dcat\Admin\Traits\HasPermissions;
 use Illuminate\Contracts\Auth\Authenticatable;
 use Illuminate\Database\Eloquent\Model;
 

+ 6 - 1
src/Traits/HasHtml.php

@@ -120,7 +120,12 @@ trait HasHtml
                 static::asset()->require(explode(',', $require));
             }
 
-            $script = "(function () {{$script}\n})();";
+            if ($init = $element->getAttribute('init')) {
+                $script = "Dcat.initialize('{$init}', function (\$this, id) { {$script}\n });";
+            } else {
+                $script = "(function () {{$script}\n})();";
+            }
+
 
             if ($element->hasAttribute('once')) {
                 return static::script($script);

+ 2 - 28
src/Widgets/Form.php

@@ -7,6 +7,7 @@ use Dcat\Admin\Admin;
 use Dcat\Admin\Contracts\LazyRenderable;
 use Dcat\Admin\Exception\RuntimeException;
 use Dcat\Admin\Form\Concerns\HandleCascadeFields;
+use Dcat\Admin\Form\Concerns\HasLayout;
 use Dcat\Admin\Form\Concerns\HasRows;
 use Dcat\Admin\Form\Concerns\HasTabs;
 use Dcat\Admin\Form\Field;
@@ -92,6 +93,7 @@ class Form implements Renderable
     use HandleCascadeFields;
     use HasRows;
     use HasTabs;
+    use HasLayout;
     use HasFormResponse {
         setCurrentUrl as defaultSetCurrentUrl;
     }
@@ -113,11 +115,6 @@ class Form implements Renderable
      */
     protected $fields;
 
-    /**
-     * @var Layout
-     */
-    protected $layout;
-
     /**
      * @var array
      */
@@ -380,29 +377,6 @@ class Form implements Renderable
         return $this->fields;
     }
 
-    /**
-     * @param int|float $width
-     * @param Closure   $callback
-     *
-     * @return $this
-     */
-    public function column($width, \Closure $callback)
-    {
-        $this->layout()->onlyColumn($width, function () use ($callback) {
-            $callback($this);
-        });
-
-        return $this;
-    }
-
-    /**
-     * @return Layout
-     */
-    public function layout()
-    {
-        return $this->layout ?: ($this->layout = new Layout($this));
-    }
-
     /**
      * Validate this form fields.
      *

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików