├── BaseObect.php
├── views
└── crop-image.php
├── CropAsset.php
├── assets
├── css
│ └── tabularInput.css
└── js
│ ├── dcropbox.js
│ └── tabularInput.js
├── bower.json
├── TabularAsset.php
├── composer.json
├── SerialColumn.php
├── ButtonColumn.php
├── LICENSE
├── MultipleTrait.php
├── Column.php
├── CropImage.php
├── README.md
├── ActionColumn.php
├── TabularInput.php
├── GridInput.php
├── DataColumn.php
├── TabularWidget.php
└── ModelHelper.php
/BaseObect.php:
--------------------------------------------------------------------------------
1 |
13 | * @since 1.3
14 | */
15 | class BaseObect extends \yii\base\BaseObject
16 | {
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/views/crop-image.php:
--------------------------------------------------------------------------------
1 |
8 |
9 |
>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | = $fileInput ?>
18 |
--------------------------------------------------------------------------------
/CropAsset.php:
--------------------------------------------------------------------------------
1 |
9 | * @since 1.0
10 | */
11 | class CropAsset extends \yii\web\AssetBundle
12 | {
13 | public $sourcePath = '@mdm/widgets/assets';
14 | public $js = [
15 | 'http://jcrop-cdn.tapmodo.com/v0.9.12/js/jquery.Jcrop.min.js',
16 | 'js/dcropbox.js'
17 | ];
18 | public $css = [
19 | 'http://jcrop-cdn.tapmodo.com/v0.9.12/css/jquery.Jcrop.min.css'
20 | ];
21 |
22 | public $depends = [
23 | 'yii\web\JqueryAsset',
24 | ];
25 | }
26 |
--------------------------------------------------------------------------------
/assets/css/tabularInput.css:
--------------------------------------------------------------------------------
1 | /*
2 | To change this license header, choose License Headers in Project Properties.
3 | To change this template file, choose Tools | Templates
4 | and open the template in the editor.
5 | */
6 | /*
7 | Created on : Nov 5, 2014, 6:01:42 AM
8 | Author : Misbahul D Munir
9 | */
10 | .tabular{
11 | display: table;
12 | }
13 |
14 | .tabular > .table-row{
15 | display: table-row;
16 | }
17 |
18 | .tabular > .table-row > .table-cell{
19 | display: table-cell;
20 | }
21 |
22 | .table-striped > .table-row:nth-child(2n+1) > .table-cell{
23 | background-color: #f3f4f5;
24 | }
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mdm-widgets",
3 | "version": "1.0.0",
4 | "homepage": "http://www.yiiframework.com/",
5 | "authors": [
6 | "Misbahul D Munir "
7 | ],
8 | "description": "Widget for yii2 framework",
9 | "keywords": [
10 | "yii2",
11 | "tabular",
12 | "input"
13 | ],
14 | "license": "BSD-3-Clause",
15 | "main": [
16 | "assets/js/tabularInput.js",
17 | "assets/css/tabularInput.css"
18 | ],
19 | "ignore": [
20 | "/*",
21 | "!/assets"
22 | ],
23 | "dependencies": {
24 | "yii2": "master"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/TabularAsset.php:
--------------------------------------------------------------------------------
1 |
11 | * @since 1.0
12 | */
13 | class TabularAsset extends AssetBundle
14 | {
15 | /**
16 | * @inheritdoc
17 | */
18 | public $sourcePath = '@mdm/widgets/assets';
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | public $css = [
24 | 'css/tabularInput.css'
25 | ];
26 |
27 | /**
28 | * @inheritdoc
29 | */
30 | public $js = [
31 | 'js/tabularInput.js'
32 | ];
33 |
34 | /**
35 | * @inheritdoc
36 | */
37 | public $depends = [
38 | 'yii\web\JqueryAsset'
39 | ];
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mdmsoft/yii2-widgets",
3 | "description": "Widgets for Yii2",
4 | "keywords": ["yii", "widgets", "tabular input"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/mdmsoft/yii2-widgets/issues",
9 | "source": "https://github.com/mdmsoft/yii2-widgets"
10 | },
11 | "authors": [
12 | {
13 | "name": "Misbahul Munir",
14 | "email": "misbahuldmunir@gmail.com"
15 | }
16 | ],
17 | "require": {
18 | "yiisoft/yii2": "~2.0.4"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "mdm\\widgets\\": ""
23 | }
24 | },
25 | "extra": {
26 | "branch-alias": {
27 | "dev-master": "1.x-dev"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SerialColumn.php:
--------------------------------------------------------------------------------
1 |
11 | * @since 1.0
12 | */
13 | class SerialColumn extends Column
14 | {
15 | /**
16 | * @inheritdoc
17 | */
18 | public $header = '#';
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | public function init()
24 | {
25 | if (!isset($this->grid->clientOptions['serialSelector'])) {
26 | Html::addCssClass($this->contentOptions, 'serial');
27 | $this->grid->clientOptions['serialSelector'] = 'td.serial';
28 | }
29 | if ($this->value === null) {
30 | $this->value = function($model, $key, $index) {
31 | return is_int($index) ? ($index + 1) : $index;
32 | };
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ButtonColumn.php:
--------------------------------------------------------------------------------
1 |
11 | * @since 1.0
12 | */
13 | class ButtonColumn extends Column
14 | {
15 | /**
16 | *
17 | * @var boolean Show add button
18 | */
19 | public $showButtonAdd = true;
20 | /**
21 | * @var string Icon for header
22 | */
23 | public $headerIcon = '';
24 | /**
25 | * @var string Icon for delete button
26 | */
27 | public $deleteIcon = '';
28 |
29 | /**
30 | * @inheritdoc
31 | */
32 | public function init()
33 | {
34 | if ($this->showButtonAdd) {
35 | $id = $this->grid->options['id'] . '-add-button';
36 | $this->header = Html::a($this->headerIcon, '#', ['id' => $id]);
37 | if (!isset($this->grid->clientOptions['btnAddSelector'])) {
38 | $this->grid->clientOptions['btnAddSelector'] = '#' . $id;
39 | }
40 | }
41 | $this->value = Html::a($this->deleteIcon, '#', ['data-action' => 'delete']);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, mdmsoft
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
--------------------------------------------------------------------------------
/MultipleTrait.php:
--------------------------------------------------------------------------------
1 |
10 | * @since 1.0
11 | */
12 | trait MultipleTrait
13 | {
14 |
15 | /**
16 | * Populates a set of models with the data from end user.
17 | * This method is mainly used to collect tabular data input.
18 | * The data to be loaded for each model is `$data[formName][index]`, where `formName`
19 | * refers to the sort name of model class, and `index` the index of the model in the `$data` array.
20 | * If `$formName` is empty, `$data[index]` will be used to populate each model.
21 | * The data being populated to each model is subject to the safety check by [[setAttributes()]].
22 | * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array
23 | * supplied by end user.
24 | * @param string $formName the form name to be used for loading the data into the models.
25 | * If not set, it will use the sort name of called class.
26 | * @param Model[] $origin original models to be populated. It will be check using `$keys` with supplied data.
27 | * If same then will be used for result model.
28 | * @param array $options Option to model
29 | * - 'scenario' for model.
30 | * - 'arguments' The parameters to be passed to the class constructor as an array.
31 | * @return boolean|Model[] whether at least one of the models is successfully populated.
32 | */
33 | public static function createMultiple($data, $formName = null, &$origin = [], $options = [])
34 | {
35 | return ModelHelper::createMultiple(get_called_class(), $data, $formName, $origin, $options);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Column.php:
--------------------------------------------------------------------------------
1 |
13 | * @since 1.0
14 | */
15 | class Column extends BaseObect
16 | {
17 | /**
18 | * @var GridInput
19 | */
20 | public $grid;
21 | /**
22 | *
23 | * @var string|\Closure
24 | */
25 | public $value;
26 | /**
27 | *
28 | * @var string header text
29 | */
30 | public $header;
31 | /**
32 | *
33 | * @var string footer text
34 | */
35 | public $footer;
36 | /**
37 | *
38 | * @var array
39 | */
40 | public $headerOptions = [];
41 | /**
42 | *
43 | * @var array
44 | */
45 | public $contentOptions = [];
46 | /**
47 | *
48 | * @var array
49 | */
50 | public $footerOptions = [];
51 | /**
52 | * @var string
53 | */
54 | public $format;
55 |
56 | /**
57 | * Render header cell
58 | * @return string
59 | */
60 | public function renderHeaderCell()
61 | {
62 | return Html::tag('th', $this->header, $this->headerOptions);
63 | }
64 |
65 | /**
66 | * Render footer cell
67 | * @return string
68 | */
69 | public function renderFooterCell()
70 | {
71 | return Html::tag('td', $this->footer, $this->footerOptions);
72 | }
73 |
74 | /**
75 | * Render data cell
76 | * @param Model $model model for cell
77 | * @param string $key
78 | * @param integer $index
79 | * @return string
80 | */
81 | public function renderDataCell($model, $key, $index)
82 | {
83 | if (is_callable($this->value)) {
84 | $value = call_user_func($this->value, $model, $key, $index);
85 | } else {
86 | $value = $this->value;
87 | }
88 | if ($this->format !== null) {
89 | $value = Yii::$app->getFormatter()->format($value, $this->format);
90 | }
91 | return Html::tag('td', $value, $this->contentOptions);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/CropImage.php:
--------------------------------------------------------------------------------
1 |
13 | * @since 1.0
14 | */
15 | class CropImage extends InputWidget
16 | {
17 | /**
18 | * @var array the HTML attributes for the input tag.
19 | * @see Html::renderTagAttributes() for details on how attributes are being rendered.
20 | */
21 | public $imgOptions = [];
22 |
23 | /**
24 | *
25 | * @var array
26 | */
27 | public $clientOptions = [];
28 | /**
29 | *
30 | * @var string
31 | */
32 | public $cropParam = 'crop';
33 |
34 | /**
35 | * @inheritdoc
36 | */
37 | public function init()
38 | {
39 |
40 | parent::init();
41 | }
42 |
43 | /**
44 | * @inheritdoc
45 | */
46 | public function run()
47 | {
48 | $id = $this->options['id'] = $this->getId();
49 |
50 | Html::addCssClass($this->options, 'dcorpbox');
51 | Html::addCssClass($this->imgOptions, 'content');
52 | CropAsset::register($this->getView());
53 |
54 | $clientOptions = $this->clientOptions;
55 | $this->imgOptions['id'] = $id . '-img';
56 | $clientOptions['imgTemplate'] = Html::tag('img', '',$this->imgOptions);
57 | $opts = Json::encode($clientOptions);
58 | $js = "jQuery('#{$id}').dCropBox($opts);";
59 | $this->getView()->registerJs($js);
60 | $inputOptions = ['style' => 'visibility:hidden;', 'class' => 'file-input', 'id' => $id . '-file'];
61 | if ($this->hasModel()) {
62 | $fileInput = Html::activeFileInput($this->model, $this->attribute, $inputOptions);
63 | } else {
64 | $fileInput = Html::fileInput($this->name, $this->value, $inputOptions);
65 | }
66 | return $this->render('crop-image', [
67 | 'options' => $this->options,
68 | 'fileInput' => $fileInput,
69 | 'cropParam' => $this->cropParam,
70 | ]);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | yii2-widgets
2 | ============
3 |
4 | Installation
5 | ------------
6 |
7 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
8 |
9 | Either run
10 |
11 | ```
12 | php composer.phar require mdmsoft/yii2-widgets "~1.0"
13 | ```
14 |
15 | or add
16 |
17 | ```
18 | "mdmsoft/yii2-widgets": "~1.0"
19 | ```
20 |
21 | to the require section of your `composer.json` file.
22 |
23 | Usage
24 | -----
25 |
26 | # TabularInput Widget
27 |
28 | `_form.php`
29 | ```php
30 |
31 |
32 |
33 |
34 | | |
35 | |
36 | |
37 |
38 |
39 | =
40 | TabularInput::widget([
41 | 'id' => 'detail-grid',
42 | 'allModels' => $model->items,
43 | 'model' => OrderItem::className(),
44 | 'tag' => 'tbody',
45 | 'form' => $form,
46 | 'itemOptions' => ['tag' => 'tr'],
47 | 'itemView' => '_item_detail',
48 | 'clientOptions' => [
49 | 'btnAddSelector' => '#btn-add',
50 | ]
51 | ]);
52 | ?>
53 |
54 | ```
55 |
56 | `_item_detail.php`
57 | ```php
58 | = $form->field($model,"[$key]product_id")->textInput()->label(false); ?> |
59 | = $form->field($model,"[$key]qty")->textInput()->label(false); ?> |
60 | |
61 | ```
62 |
63 | # GridInput Widget
64 | ```php
65 | =
66 | GridInput::widget([
67 | 'id' => 'detail-grid',
68 | 'allModels' => $model->items,
69 | 'model' => OrderItem::className(),
70 | 'columns' => [
71 | ['class' => 'mdm\widgets\SerialColumn'],
72 | 'product_id',
73 | 'qty',
74 | [
75 | 'attribute' => 'uom_id',
76 | 'items' => [
77 | 1 => 'Pcs',
78 | 2 => 'Dozen'
79 | ]
80 | ],
81 | [
82 | 'attribute' => 'tax',
83 | 'type' => 'checkbox',
84 | ],
85 | ['class' => 'mdm\widgets\ButtonColumn']
86 | ],
87 | ]);
88 | ?>
89 | ```
--------------------------------------------------------------------------------
/ActionColumn.php:
--------------------------------------------------------------------------------
1 |
12 | * @since 1.0
13 | */
14 | class ActionColumn extends \yii\grid\ActionColumn
15 | {
16 | public $iconTemplate = '';
17 |
18 | /**
19 | * Initializes the default button rendering callbacks.
20 | */
21 | protected function initDefaultButtons()
22 | {
23 | $buttons = [
24 | 'view' => [
25 | 'title' => Yii::t('yii', 'View'),
26 | 'aria-label' => Yii::t('yii', 'View'),
27 | 'data-pjax' => '0',
28 | 'icon' => 'eye-open',
29 | ],
30 | 'update' => [
31 | 'title' => Yii::t('yii', 'Update'),
32 | 'aria-label' => Yii::t('yii', 'Update'),
33 | 'data-pjax' => '0',
34 | 'icon' => 'pencil'
35 | ],
36 | 'delete' => [
37 | 'title' => Yii::t('yii', 'Delete'),
38 | 'aria-label' => Yii::t('yii', 'Delete'),
39 | 'data-confirm' => Yii::t('yii', 'Are you sure you want to delete this item?'),
40 | 'data-method' => 'post',
41 | 'data-pjax' => '0',
42 | 'icon' => 'trash'
43 | ]
44 | ];
45 | foreach ($buttons as $name => $button) {
46 | $this->buttons[$name] = array_merge($button, $this->buttonOptions, $this->buttons[$name]);
47 | }
48 | }
49 |
50 | /**
51 | * @inheritdoc
52 | */
53 | protected function renderDataCellContent($model, $key, $index)
54 | {
55 | return preg_replace_callback('/\\{([\w\-\/]+)\\}/', function ($matches) use ($model, $key, $index) {
56 | $name = $matches[1];
57 |
58 | if (isset($this->visibleButtons[$name])) {
59 | $isVisible = $this->visibleButtons[$name] instanceof \Closure ? call_user_func($this->visibleButtons[$name], $model, $key, $index)
60 | : $this->visibleButtons[$name];
61 | } else {
62 | $isVisible = true;
63 | }
64 |
65 | if ($isVisible) {
66 | $button = isset($this->buttons[$name]) ? $this->buttons[$name] : [
67 | 'label' => ucfirst($name),
68 | 'aria-label' => ucfirst($name),
69 | 'data-pjax' => '0',
70 | ];
71 | $url = $this->createUrl($name, $model, $key, $index);
72 | if ($button instanceof \Closure) {
73 | return call_user_func($button, $url, $model, $key);
74 | } else {
75 | $icon = str_replace('{icon}', ArrayHelper::remove($button, 'icon', ''), $this->iconTemplate);
76 | return Html::a($icon, $url, $button);
77 | }
78 | }
79 | return '';
80 | }, $this->template);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/assets/js/dcropbox.js:
--------------------------------------------------------------------------------
1 | (function () {
2 |
3 | function updateCoords(c) {
4 | var $element = this.$el;
5 | var factor = this.zoomFactor;
6 | $element.find('input[data-attr="x"]').val(c.x * factor);
7 | $element.find('input[data-attr="y"]').val(c.y * factor);
8 | $element.find('input[data-attr="w"]').val(c.w * factor);
9 | $element.find('input[data-attr="h"]').val(c.h * factor);
10 | }
11 |
12 | function inputChange() {
13 | var input = this;
14 | var $el = $(this).closest('div.dcorpbox');
15 | var id = $el.attr('id');
16 | var imgId = id + '-img';
17 | var opts = $el.data('dCropBox');
18 |
19 | if (input.files && input.files[0]) {
20 | if (opts.api) {
21 | opts.api.destroy();
22 | }
23 | $el.children('div.container').html(opts.imgTemplate);
24 |
25 | var reader = new FileReader();
26 | reader.onload = function (e) {
27 | $el.trigger('beforeLoadFile');
28 | $el.find('input[data-attr="x"]').val('');
29 | var $img = $('#' + imgId);
30 | $img.attr('src', e.target.result);
31 | var img = new Image();
32 |
33 | img.onload = function () {
34 | if ((opts.minWidth && img.width < opts.minWidth) ||
35 | (opts.minHeight && img.width < opts.minHeight)) {
36 | alert(opts.toSmallMsg);
37 | return;
38 | }
39 | var factor = img.width / $img.width();
40 | var params = {
41 | onSelect: updateCoords,
42 | };
43 | var selection;
44 | if (opts.minWidth || opts.minHeight) {
45 | var minW = opts.minWidth / factor;
46 | var minH = opts.minHeight / factor;
47 | selection = [0, 0, minW, minH];
48 | params = $.extend({}, params, {
49 | minSize: [minW, minH],
50 | });
51 | }
52 | opts.api = $.Jcrop('#' + imgId, $.extend({}, params, {
53 | aspectRatio: opts.aspectRatio,
54 | }, opts.jcrop || {}));
55 | opts.api.$el = $el;
56 | opts.api.zoomFactor = factor;
57 |
58 | if (selection){
59 | opts.api.setSelect(selection);
60 | }
61 | $el.trigger('afterLoadFile');
62 | }
63 | img.src = e.target.result;
64 | }
65 | reader.readAsDataURL(input.files[0]);
66 | }
67 | }
68 |
69 | var methods = {
70 | init: function (options) {
71 | return this.each(function () {
72 | var $this = $(this);
73 | var opts = $.extend({}, defaults, options || {});
74 |
75 | if (opts.minWidth && opts.minHeight == undefined) {
76 | opts.minHeight = opts.minWidth / opts.aspectRatio;
77 | } else if (opts.minHeight && opts.minWidth == undefined) {
78 | opts.minWidth = opts.minHeight * opts.aspectRatio;
79 | }
80 | if(opts.button){
81 | $(opts.button).click(function (){
82 | methods.selectFile.call($this);
83 | });
84 | }
85 | $this.data('dCropBox', opts);
86 | $this.children(':input.file-input').change(inputChange);
87 | });
88 | },
89 | selectFile: function () {
90 | return this.each(function () {
91 | $(this).children(':input.file-input').trigger('click');
92 | });
93 | }
94 | }
95 |
96 | var defaults = {
97 | aspectRatio: 1,
98 | toSmallMsg: 'Image to small',
99 | }
100 |
101 | $.fn.dCropBox = function (method) {
102 | if (methods[method]) {
103 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
104 | } else if (typeof method === 'object' || !method) {
105 | return methods.init.apply(this, arguments);
106 | } else {
107 | $.error('Method ' + method + ' does not exist on jQuery.dCropBox');
108 | return false;
109 | }
110 | }
111 | })();
--------------------------------------------------------------------------------
/TabularInput.php:
--------------------------------------------------------------------------------
1 |
14 | * @since 1.0
15 | */
16 | class TabularInput extends TabularWidget
17 | {
18 | /**
19 | * @var string|callable the name of the view for rendering each data item, or a callback (e.g. an anonymous function)
20 | * for rendering each data item. If it specifies a view name, the following variables will
21 | * be available in the view:
22 | *
23 | * - `$model`: mixed, the data model
24 | * - `$key`: mixed, the key value associated with the data item
25 | * - `$index`: integer, the zero-based index of the data item in the items array returned by [[dataProvider]].
26 | * - `$widget`: ListView, this widget instance
27 | *
28 | * Note that the view name is resolved into the view file by the current context of the [[view]] object.
29 | *
30 | * If this property is specified as a callback, it should have the following signature:
31 | *
32 | * ~~~
33 | * function ($model, $key, $index, $widget)
34 | * ~~~
35 | */
36 | public $itemView;
37 | /**
38 | * @var array additional parameters to be passed to [[itemView]] when it is being rendered.
39 | * This property is used only when [[itemView]] is a string representing a view name.
40 | */
41 | public $viewParams = [];
42 | /**
43 | * @var array
44 | */
45 | public $tags = [
46 | '<@@' => ' '=',
48 | '@>' => '?>',
49 | ];
50 | /**
51 | * @var string
52 | */
53 | private $_templateFile;
54 |
55 | /**
56 | * @inheritdoc
57 | */
58 | public function init()
59 | {
60 | parent::init();
61 | ob_start();
62 | ob_implicit_flush(false);
63 | }
64 |
65 | /**
66 | * @inheritdoc
67 | */
68 | public function run()
69 | {
70 | $template = trim(ob_get_clean());
71 | if ($this->itemView === null && !empty($template)) {
72 | $current = $this->getView()->getViewFile();
73 | $file = sprintf('%x/%x-%s', crc32(dirname($current)) % 0x100, crc32($current), $this->options['id']);
74 | $this->_templateFile = Yii::getAlias("@runtime/mdm-tabular/{$file}.php");
75 | if (!is_file($this->_templateFile) || filemtime($current) >= filemtime($this->_templateFile)) {
76 | FileHelper::createDirectory(dirname($this->_templateFile));
77 | $template = str_replace(array_keys($this->tags), array_values($this->tags), $template);
78 | file_put_contents($this->_templateFile, $template, LOCK_EX);
79 | }
80 | }
81 | if (empty($this->clientOptions['itemSelector']) && ($tag = ArrayHelper::getValue($this->itemOptions, 'tag', 'div'))) {
82 | Html::addCssClass($this->itemOptions, "mdm-item{$this->level}");
83 | $this->clientOptions['itemSelector'] = "{$tag}.mdm-item{$this->level}";
84 | }
85 | parent::run();
86 | }
87 |
88 | /**
89 | * @inheritdoc
90 | */
91 | public function renderItem($model, $key, $index)
92 | {
93 | $params = array_merge([
94 | 'model' => $model,
95 | 'key' => $key,
96 | 'index' => $index,
97 | 'widget' => $this,
98 | 'form' => $this->form,
99 | ], $this->viewParams);
100 |
101 | // render content
102 | if ($this->itemView === null) {
103 | $content = $this->_templateFile ? $this->template($params) : $key;
104 | } elseif (is_string($this->itemView)) {
105 | $content = $this->getView()->render($this->itemView, $params);
106 | } else {
107 | $content = call_user_func($this->itemView, $model, $key, $index, $this);
108 | }
109 | $options = $this->itemOptions;
110 | $tag = ArrayHelper::remove($options, 'tag', 'div');
111 | if ($tag === false) {
112 | return $content;
113 | }
114 | $options['data-key'] = (string) $key;
115 | $options['data-index'] = (string) $index;
116 | return Html::tag($tag, $content, $options);
117 | }
118 |
119 | /**
120 | * Render template
121 | * @param array $params
122 | * @return string
123 | */
124 | protected function template($params = [])
125 | {
126 | return $this->getView()->renderPhpFile($this->_templateFile, $params);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/GridInput.php:
--------------------------------------------------------------------------------
1 |
15 | * @since 1.0
16 | */
17 | class GridInput extends TabularWidget
18 | {
19 | /**
20 | * @inheritdoc
21 | */
22 | public $tag = 'table';
23 |
24 | /**
25 | * @var Column[]
26 | */
27 | public $columns = [];
28 |
29 | /**
30 | * @var array
31 | */
32 | public $hiddens = [];
33 |
34 | /**
35 | *
36 | * @var string
37 | */
38 | public $header;
39 |
40 | /**
41 | *
42 | * @var string
43 | */
44 | public $footer;
45 |
46 | /**
47 | *
48 | * @var array
49 | */
50 | public $headerOptions = [];
51 |
52 | /**
53 | *
54 | * @var array
55 | */
56 | public $footerOptions = [];
57 |
58 | /**
59 | *
60 | * @var string
61 | */
62 | public $defaultColumnClass = 'mdm\widgets\DataColumn';
63 |
64 | /**
65 | * @inheritdoc
66 | */
67 | public $options = ['class' => 'table table-striped'];
68 |
69 | /**
70 | * @inheritdoc
71 | */
72 | public function init()
73 | {
74 | parent::init();
75 | if (!($this->model instanceof Model)) {
76 | $property = __CLASS__ . '::$model';
77 | throw new InvalidConfigException("Value of \"{$property}\" must be specified.");
78 | }
79 | foreach ($this->columns as $i => $column) {
80 | if (is_string($column)) {
81 | $column = Yii::createObject([
82 | 'class' => $this->defaultColumnClass,
83 | 'attribute' => $column,
84 | 'grid' => $this,
85 | ]);
86 | } elseif (is_array($column)) {
87 | if (!isset($column['class'])) {
88 | $column['class'] = $this->defaultColumnClass;
89 | }
90 | $column['grid'] = $this;
91 | $column = Yii::createObject($column);
92 | }
93 |
94 | $this->columns[$i] = $column;
95 | }
96 | $this->containerOptions['tag'] = 'tbody';
97 | $this->clientOptions = array_merge([
98 | 'container' => "tbody.mdm-container{$this->level}",
99 | 'itemSelector' => "tr.mdm-item{$this->level}"
100 | ], $this->clientOptions);
101 | Html::addCssClass($this->itemOptions, "mdm-item{$this->level}");
102 | Html::addCssClass($this->containerOptions, "mdm-container{$this->level}");
103 | }
104 |
105 | /**
106 | * @inheritdoc
107 | */
108 | public function renderHeader()
109 | {
110 | if ($this->header === false) {
111 | return '';
112 | }
113 | if ($this->header === null) {
114 | $cols = [];
115 | foreach ($this->columns as $column) {
116 | $cols[] = $column->renderHeaderCell();
117 | }
118 | if (count($this->hiddens)) {
119 | $cols[] = ' | ';
120 | }
121 | $rows = [Html::tag('tr', implode("\n", $cols), $this->headerOptions)];
122 | } else {
123 | $rows = [];
124 | foreach ((array) $this->header as $header) {
125 | $rows[] = Html::tag('tr', $header, $this->headerOptions);
126 | }
127 | }
128 | return Html::tag('thead', implode("\n", $rows));
129 | }
130 |
131 | /**
132 | * @inheritdoc
133 | */
134 | public function renderItem($model, $key, $index)
135 | {
136 | $cols = [];
137 | /* @var $column Column */
138 | foreach ($this->columns as $column) {
139 | $cols[] = $column->renderDataCell($model, $key, $index);
140 | }
141 | if (count($this->hiddens)) {
142 | $hiddens = [];
143 | foreach ($this->hiddens as $options) {
144 | if (is_string($options)) {
145 | $attribute = $options;
146 | $options = [];
147 | } else {
148 | $attribute = ArrayHelper::remove($options, 'attribute');
149 | }
150 | $field = str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], $attribute);
151 | $options['data-field'] = $field;
152 | $options['id'] = false;
153 | $hiddens[] = Html::activeHiddenInput($model, "[$key]{$attribute}", $options);
154 | }
155 | $cols[] = Html::tag('td', implode("\n", $hiddens), ['style' => ['display' => 'none'], 'class' => 'hidden-col']);
156 | }
157 | $options = $this->itemOptions;
158 | $options['data-key'] = (string) $key;
159 | $options['data-index'] = (string) $index;
160 | return Html::tag('tr', implode("\n", $cols), $options);
161 | }
162 |
163 | /**
164 | * @inheritdoc
165 | */
166 | public function renderFooter()
167 | {
168 | if ($this->footer === false) {
169 | return '';
170 | }
171 | if ($this->footer === null) {
172 | $cols = [];
173 | foreach ($this->columns as $column) {
174 | $cols[] = $column->renderFooterCell();
175 | }
176 | if (count($this->hiddens)) {
177 | $cols[] = ' | ';
178 | }
179 | $rows = [Html::tag('tr', implode("\n", $cols), $this->footerOptions)];
180 | } else {
181 | $rows = [];
182 | foreach ((array) $this->footer as $footer) {
183 | $rows[] = Html::tag('tr', $footer, $this->footerOptions);
184 | }
185 | }
186 | return Html::tag('tfoot', implode("\n", $rows));
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/DataColumn.php:
--------------------------------------------------------------------------------
1 |
15 | * @since 1.0
16 | */
17 | class DataColumn extends Column
18 | {
19 | /**
20 | * @var string attribute
21 | */
22 | public $attribute;
23 |
24 | /**
25 | * @var array option for input
26 | */
27 | public $inputOptions = ['class' => 'form-control'];
28 |
29 | /**
30 | * @var array|Closure
31 | */
32 | public $items;
33 |
34 | /**
35 | * @var string
36 | */
37 | public $template = '{input} {error}';
38 |
39 | /**
40 | * @var string|array
41 | * ```php
42 | *
43 | * ```
44 | */
45 | public $widget;
46 |
47 | /**
48 | *
49 | * @var string
50 | */
51 | public $type = 'text';
52 |
53 | /**
54 | * @inheritdoc
55 | */
56 | public function init()
57 | {
58 | if ($this->attribute) {
59 | $field = str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], $this->attribute);
60 | } else {
61 | $field = false;
62 | }
63 | if (empty($this->inputOptions['data-field']) && $field) {
64 | $this->inputOptions['data-field'] = $field;
65 | }
66 | if (empty($this->contentOptions['data-column']) && $field) {
67 | $this->contentOptions['data-column'] = $field;
68 | }
69 | if (empty($this->headerOptions['data-column']) && $field) {
70 | $this->headerOptions['data-column'] = $field;
71 | }
72 | if ($this->header === null) {
73 | if ($this->grid->model instanceof Model && !empty($this->attribute)) {
74 | $this->header = $this->grid->model->getAttributeLabel($this->attribute);
75 | } else {
76 | $this->header = Inflector::camel2words($this->attribute);
77 | }
78 | }
79 | if ($this->value === null) {
80 | $this->value = [$this, 'renderInputCell'];
81 | } elseif (is_string($this->value)) {
82 | $this->attribute = $this->value;
83 | $this->value = [$this, 'renderTextCell'];
84 | }
85 | }
86 |
87 | /**
88 | * Render input cell
89 | * @param Model $model model for cell
90 | * @param string $key
91 | * @param integer $index
92 | * @return string
93 | */
94 | public function renderInputCell($model, $key, $index)
95 | {
96 | $form = $this->grid->form;
97 | $items = $this->items;
98 | if ($this->widget !== null) {
99 | if (is_array($this->widget)) {
100 | list($widget, $options) = $this->widget;
101 | if ($options instanceof Closure) {
102 | $options = call_user_func($options, $model, $key, $index);
103 | }
104 | } else {
105 | $widget = $this->widget;
106 | $options = [];
107 | }
108 | if ($form instanceof ActiveForm) {
109 | return $form->field($model, "[$key]{$this->attribute}", ['template' => $this->template])
110 | ->widget($widget, $options);
111 | } else {
112 | $options = array_merge([
113 | 'model' => $model,
114 | 'attribute' => "[$key]{$this->attribute}"
115 | ], $options);
116 | return $widget::widget($options);
117 | }
118 | } elseif ($items !== null) {
119 | if ($items instanceof Closure) {
120 | $items = call_user_func($items, $model, $key, $index);
121 | }
122 | switch ($this->type) {
123 | case 'checkbox':
124 | if ($form instanceof ActiveForm) {
125 | return $form->field($model, "[$key]{$this->attribute}", ['template' => $this->template])
126 | ->checkboxList($items, $this->inputOptions);
127 | } else {
128 | return Html::activeCheckboxList($model, "[$key]{$this->attribute}", $items, $this->inputOptions);
129 | }
130 | break;
131 |
132 | default:
133 | if ($form instanceof ActiveForm) {
134 | return $form->field($model, "[$key]{$this->attribute}", ['template' => $this->template])
135 | ->dropDownList($items, $this->inputOptions);
136 | } else {
137 | return Html::activeDropDownList($model, "[$key]{$this->attribute}", $items, $this->inputOptions);
138 | }
139 | break;
140 | }
141 | } else {
142 | switch ($this->type) {
143 | case 'checkbox':
144 | if ($form instanceof ActiveForm) {
145 | return $form->field($model, "[$key]{$this->attribute}", ['template' => $this->template])
146 | ->checkbox($this->inputOptions, false);
147 | } else {
148 | return Html::activeCheckbox($model, "[$key]{$this->attribute}", $this->inputOptions);
149 | }
150 | break;
151 |
152 | default:
153 | if ($form instanceof ActiveForm) {
154 | return $form->field($model, "[$key]{$this->attribute}", ['template' => $this->template])
155 | ->textInput($this->inputOptions);
156 | } else {
157 | return Html::activeTextInput($model, "[$key]{$this->attribute}", $this->inputOptions);
158 | }
159 | break;
160 | }
161 | }
162 | }
163 |
164 | /**
165 | * Render input cell
166 | * @param Model $model model for cell
167 | * @param string $key
168 | * @return string
169 | */
170 | public function renderTextCell($model, $key)
171 | {
172 | return Html::getAttributeValue($model, "[$key]{$this->attribute}");
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/TabularWidget.php:
--------------------------------------------------------------------------------
1 |
18 | * @since 1.0
19 | */
20 | abstract class TabularWidget extends Widget
21 | {
22 | public $tag = 'div';
23 | /**
24 | * @var array the HTML attributes for the container tag of the list view.
25 | * The "tag" element specifies the tag name of the container element and defaults to "div".
26 | * @see Html::renderTagAttributes() for details on how attributes are being rendered.
27 | */
28 | public $options = [];
29 | /**
30 | * @var array the HTML attributes for the container of the rendering result of each data model.
31 | * The "tag" element specifies the tag name of the container element and defaults to "div".
32 | * If "tag" is false, it means no container element will be rendered.
33 | * @see Html::renderTagAttributes() for details on how attributes are being rendered.
34 | */
35 | public $itemOptions = [];
36 | /**
37 | * @var array the HTML attributes for the container of the rendering result of each data model.
38 | * The "tag" element specifies the tag name of the container element and defaults to "div".
39 | * If "tag" is false, it means no container element will be rendered.
40 | * @see Html::renderTagAttributes() for details on how attributes are being rendered.
41 | */
42 | public $containerOptions = [];
43 | /**
44 | * @var string the HTML code to be displayed between any two consecutive items.
45 | */
46 | public $separator = "\n";
47 | /**
48 | * @var Model[]|array
49 | */
50 | public $allModels = [];
51 | /**
52 | * @var mixed
53 | */
54 | public $model;
55 | /**
56 | * @var ActiveForm
57 | */
58 | public $form;
59 | /**
60 | * @var array Client option
61 | */
62 | public $clientOptions = [];
63 | /**
64 | * @var string
65 | */
66 | public $layout = "{header}\n{items}";
67 | /**
68 | * Header
69 | * @var string
70 | */
71 | public $header;
72 | /**
73 | * Footer
74 | * @var string
75 | */
76 | public $footer;
77 | /**
78 | * Part
79 | * @var array
80 | */
81 | public $sections = [];
82 | protected $level;
83 | private static $_level = 0;
84 |
85 | /**
86 | * Renders a single data model.
87 | * @param mixed $model the data model to be rendered
88 | * @param mixed $key the key value associated with the data model
89 | * @param integer $index the zero-based index of the data model in the model array returned by [[dataProvider]].
90 | * @return string the rendering result
91 | */
92 | abstract public function renderItem($model, $key, $index);
93 |
94 | /**
95 | * @inheritdoc
96 | */
97 | public function init()
98 | {
99 | $this->level = self::$_level === 0 ? '' : self::$_level;
100 | self::$_level++;
101 |
102 | if (!isset($this->options['id'])) {
103 | $this->options['id'] = $this->getId();
104 | }
105 | if (!empty($this->model) && !is_object($this->model)) {
106 | $this->model = Yii::createObject($this->model);
107 | }
108 | }
109 |
110 | /**
111 | * @inheritdoc
112 | */
113 | public function run()
114 | {
115 | if (!empty($this->containerOptions['tag']) && empty($this->clientOptions['container'])) {
116 | Html::addCssClass($this->containerOptions, 'mdm-container');
117 | $this->clientOptions['container'] = "{$this->containerOptions['tag']}.mdm-container";
118 | }
119 | $this->registerClientScript();
120 |
121 | $content = preg_replace_callback('/\\{([\w\-\/]+)\\}/', function ($matches) {
122 | $method = 'render' . $matches[1];
123 | if (method_exists($this, $method)) {
124 | return $this->$method();
125 | } elseif (isset($this->sections[$matches[1]])) {
126 | return $matches[1];
127 | }
128 | return $matches[0];
129 | }, $this->layout);
130 |
131 | self::$_level--;
132 | echo Html::tag($this->tag, $content, $this->options);
133 | }
134 |
135 | /**
136 | * Renders all data models.
137 | * @return string the rendering result
138 | */
139 | public function renderHeader()
140 | {
141 | return $this->header;
142 | }
143 |
144 | /**
145 | * Renders all data models.
146 | * @return string the rendering result
147 | */
148 | public function renderItems()
149 | {
150 | $rows = [];
151 | $index = 0;
152 | foreach ($this->allModels as $key => $model) {
153 | $rows[] = $this->renderItem($model, $key, $index++);
154 | }
155 | $content = implode($this->separator, $rows);
156 | $options = $this->containerOptions;
157 | if (empty($options['tag'])) {
158 | return $content;
159 | }
160 | $tag = $options['tag'];
161 | unset($options['tag']);
162 |
163 | return Html::tag($tag, $content, $options);
164 | }
165 |
166 | public function renderFooter()
167 | {
168 | return $this->footer;
169 | }
170 | /**
171 | * Register script
172 | */
173 | protected function registerClientScript()
174 | {
175 | $id = $this->options['id'];
176 | $options = Json::htmlEncode($this->getClientOptions());
177 | $view = $this->getView();
178 | TabularAsset::register($view);
179 | $view->registerJs("jQuery('#$id').mdmTabularInput($options);");
180 | }
181 |
182 | /**
183 | * Get client options
184 | * @return array
185 | */
186 | protected function getClientOptions()
187 | {
188 | $counter = count($this->allModels) ? max(array_keys($this->allModels)) + 1 : 0;
189 | $clientOptions = $this->clientOptions;
190 |
191 | $clientOptions['counter'] = $counter;
192 | if (empty($clientOptions['itemSelector'])) {
193 | throw new InvalidConfigException('Value of "clientOptions[\'itemSelector\']" must be specified.');
194 | }
195 | if ($this->form instanceof ActiveForm) {
196 | $clientOptions['formSelector'] = '#' . $this->form->options['id'];
197 | $oldAttrs = $this->form->attributes;
198 | $this->form->attributes = [];
199 | }
200 |
201 | // template and js
202 | $view = $this->getView();
203 | $oldJs = $view->js;
204 | $view->js = [];
205 |
206 | $template = $this->renderItem($this->model, "_dkey{$this->level}_", "_dindex{$this->level}_");
207 | if (isset($oldAttrs)) {
208 | $clientOptions['validations'] = $this->form->attributes;
209 | $this->form->attributes = $oldAttrs;
210 | }
211 | $js = [];
212 | ksort($view->js);
213 | foreach ($view->js as $pieces) {
214 | $js[] = implode("\n", $pieces);
215 | }
216 | if (count($js)) {
217 | $clientOptions['templateJs'] = implode("\n", $js);
218 | }
219 | $view->js = $oldJs;
220 | // ***
221 |
222 | $clientOptions['template'] = $template;
223 | $clientOptions['replaces'] = [
224 | 'key' => new JsExpression("/_dkey{$this->level}_/g"),
225 | 'index' => new JsExpression("/_dindex{$this->level}_/g"),
226 | ];
227 | return $clientOptions;
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/ModelHelper.php:
--------------------------------------------------------------------------------
1 |
15 | * @since 1.0
16 | */
17 | class ModelHelper
18 | {
19 |
20 | /**
21 | * Populates a set of models with the data from end user.
22 | * This method is mainly used to collect tabular data input.
23 | * The data to be loaded for each model is `$data[formName][index]`, where `formName`
24 | * refers to the sort name of model class, and `index` the index of the model in the `$data` array.
25 | * If `$formName` is empty, `$data[index]` will be used to populate each model.
26 | * The data being populated to each model is subject to the safety check by [[setAttributes()]].
27 | * @param string $class Model class name.
28 | * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array
29 | * supplied by end user.
30 | * @param string $formName the form name to be used for loading the data into the models.
31 | * If not set, it will use the sort name of called class.
32 | * @param Model[] $origin original models to be populated. It will be check using `$keys` with supplied data.
33 | * If same then will be used for result model.
34 | * @param array $options Option to model
35 | * - scenario for model.
36 | * - arguments The parameters to be passed to the class constructor as an array.
37 | * @return boolean|Model[] whether at least one of the models is successfully populated.
38 | */
39 | public static function createMultiple($class, $data, $formName = null, &$origin = [], $options = [])
40 | {
41 | $reflector = new ReflectionClass($class);
42 | $args = isset($options['arguments']) ? $options['arguments'] : [];
43 | if ($formName === null) {
44 | /* @var $model Model */
45 | $model = empty($args) ? new $class() : $reflector->newInstanceArgs($args);
46 | $formName = $model->formName();
47 | }
48 | if ($formName != '') {
49 | $data = isset($data[$formName]) ? $data[$formName] : null;
50 | }
51 | if ($data === null) {
52 | return false;
53 | }
54 |
55 | $models = [];
56 | foreach ($data as $i => $row) {
57 | $model = null;
58 | if (isset($origin[$i])) {
59 | $model = $origin[$i];
60 | unset($origin[$i]);
61 | } else {
62 | $model = empty($args) ? new $class() : $reflector->newInstanceArgs($args);
63 | }
64 | if (isset($options['scenario'])) {
65 | $model->scenario = $options['scenario'];
66 | }
67 | $model->load($row, '');
68 | $models[$i] = $model;
69 | }
70 | return $models;
71 | }
72 |
73 | /**
74 | * Populates a set of models with the data from end user.
75 | * This method is mainly used to collect tabular data input.
76 | * The data to be loaded for each model is `$data[formName][index]`, where `formName`
77 | * refers to the sort name of model class, and `index` the index of the model in the `$data` array.
78 | * If `$formName` is empty, `$data[index]` will be used to populate each model.
79 | * The data being populated to each model is subject to the safety check by [[setAttributes()]].
80 | * @param string $class Model class name.
81 | * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array
82 | * supplied by end user.
83 | * @param string $formName the form name to be used for loading the data into the models.
84 | * If not set, it will use the sort name of called class.
85 | * @param Model[] $origin original models to be populated. It will be check using `$keys` with supplied data.
86 | * If same then will be used for result model.
87 | * @param array $config Option to model
88 | * @return boolean|Model[] whether at least one of the models is successfully populated.
89 | */
90 | public static function loadMultiple($class, $data, $formName = null, &$origin = [], $config = [])
91 | {
92 | $config['class'] = $class;
93 | if ($formName === null) {
94 | /* @var $model Model */
95 | $model = Yii::createObject($config);
96 | $formName = $model->formName();
97 | }
98 | if ($formName != '') {
99 | $data = isset($data[$formName]) ? $data[$formName] : null;
100 | }
101 | if ($data === null) {
102 | return [];
103 | }
104 |
105 | $models = [];
106 | foreach ($data as $i => $row) {
107 | $model = null;
108 | if (isset($origin[$i])) {
109 | $model = $origin[$i];
110 | unset($origin[$i]);
111 | } else {
112 | $model = Yii::createObject($config);
113 | }
114 | $model->load($row, '');
115 | $models[$i] = $model;
116 | }
117 | return $models;
118 | }
119 |
120 | /**
121 | *
122 | * @param Model[] $models
123 | * @param array $options
124 | * @param array $messages
125 | * @return boolean
126 | */
127 | public static function validateMultiple(array $models, $options = [], &$messages = [])
128 | {
129 | /* @var $model Model */
130 | $validateAll = !empty($options['validateAll']);
131 | /* @var $model BaseActiveRecord */
132 | if (isset($options['values'])) {
133 | foreach ($models as $model) {
134 | Yii::configure($model, $options['values']);
135 | }
136 | }
137 | $result = true;
138 | foreach ($models as $i => $model) {
139 | $oke = !isset($options['beforeValidate']) || call_user_func($options['beforeValidate'], $model) !== false;
140 | if ($oke && $model->validate()) {
141 | isset($options['afterValidate']) && call_user_func($options['afterValidate'], $model);
142 | } else {
143 | foreach ($model->getErrors() as $attribute => $message) {
144 | $messages[Html::getInputId($model, "[$i]$attribute")] = $message;
145 | }
146 | if ($validateAll) {
147 | $result = false;
148 | } else {
149 | return false;
150 | }
151 | }
152 | }
153 | return $result;
154 | }
155 |
156 | /**
157 | *
158 | * @param BaseActiveRecord[] $models
159 | * @param array $options
160 | * - runValidation boolean default true.
161 | * - beforeSave callable
162 | * - afterSave callable
163 | * - deleteUnsaved boolean default true.
164 | * @param array $messages
165 | * @return boolean
166 | */
167 | public static function saveMultiple(array $models, $options = [], &$messages = [])
168 | {
169 | $runValidation = !isset($options['runValidation']) || $options['runValidation'];
170 | /* @var $model BaseActiveRecord */
171 | if (isset($options['values'])) {
172 | foreach ($models as $model) {
173 | Yii::configure($model, $options['values']);
174 | }
175 | }
176 | unset($options['values']);
177 | if (!$runValidation || static::validateMultiple($models, $options, $messages)) {
178 | foreach ($models as $model) {
179 | if (!isset($options['beforeSave']) || call_user_func($options['beforeSave'], $model) !== false) {
180 | if ($model->save(false)) {
181 | isset($options['afterSave']) && call_user_func($options['afterSave'], $model);
182 | } else {
183 | return false;
184 | }
185 | } elseif (!isset($options['deleteUnsaved']) || $options['deleteUnsaved']) {
186 | if (!$model->isNewRecord) {
187 | $model->delete();
188 | }
189 | }
190 | }
191 | return true;
192 | }
193 | return false;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/assets/js/tabularInput.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jQuery plugin for tabular input.
3 | * Allow to add and delete row.
4 | *
5 | * ```javascript
6 | * $('#id').mdmTabularInput({
7 | *
8 | * });
9 | * ```
10 | *
11 | * @author Misbahul D Munir
12 | * @since 1.0
13 | */
14 | (function ($) {
15 | $.fn.mdmTabularInput = function (method) {
16 | if (methods[method]) {
17 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
18 | } else if (typeof method === 'object' || !method) {
19 | return methods.init.apply(this, arguments);
20 | } else {
21 | $.error('Method ' + method + ' does not exist on jQuery.mdmTabularInput');
22 | return false;
23 | }
24 | };
25 |
26 | var events = {
27 | initRow: 'initRow',
28 | init: 'init',
29 | beforeAdd: 'beforeAdd',
30 | afterAdd: 'afterAdd',
31 | beforeDelete: 'beforeDelete',
32 | afterDelete: 'afterDelete',
33 | change: 'change'
34 | };
35 | var defaults = {
36 | container: undefined,
37 | template: undefined,
38 | multiSelect: false,
39 | counter: 0,
40 | btnDelSelector: '[data-action=\'delete\']',
41 | serialSelector: '.serial',
42 | btnAddSelector: undefined,
43 | itemSelector: undefined,
44 | formSelector: undefined,
45 | validations: undefined,
46 | replaces: {},
47 | };
48 |
49 | function element($e, container) {
50 | if (container) {
51 | return $e.find(container);
52 | } else {
53 | return $e;
54 | }
55 | }
56 |
57 | function replace(s, r, v) {
58 | for (var k in r) {
59 | s = s.replace(r[k], (typeof v == 'object') ? v[k] : v);
60 | }
61 | return s;
62 | }
63 |
64 | function evaluateJs(str) {
65 | eval(str);
66 | }
67 |
68 | var methods = {
69 | init: function (options) {
70 | return this.each(function () {
71 | var $e = $(this);
72 | var settings = $.extend({}, defaults, options || {});
73 | var $container = element($e, settings.container);
74 |
75 | $e.data('mdmTabularInput', {settings: settings});
76 | // add button
77 | if (settings.btnAddSelector) {
78 | $(settings.btnAddSelector)
79 | .off('click.mdmTabularInput')
80 | .on('click.mdmTabularInput', function (event) {
81 | $e.mdmTabularInput('addRow');
82 | event.preventDefault();
83 | return false;
84 | });
85 | }
86 | // delete button
87 | if (settings.btnDelSelector) {
88 | $container
89 | .off('click.mdmTabularInput', settings.btnDelSelector)
90 | .on('click.mdmTabularInput', settings.btnDelSelector, function (event) {
91 | $e.mdmTabularInput('deleteRow', $(this).closest(settings.itemSelector));
92 | event.preventDefault();
93 | return false;
94 | });
95 | }
96 | // select/togle row by click
97 | $container
98 | .off('click.mdmTabularInput', settings.itemSelector)
99 | .on('click.mdmTabularInput', settings.itemSelector, function (e) {
100 | var $this = $(this);
101 | if ($this.is(settings.itemSelector)) {
102 | $e.mdmTabularInput(e.ctrlKey ? 'toggleSelectRow' : 'selectRow', $this);
103 | }
104 | });
105 |
106 | $container
107 | .children(settings.itemSelector).each(function () {
108 | $e.trigger(events.initRow, [$(this)]);
109 | });
110 | $e.trigger(events.init);
111 |
112 | $e.mdmTabularInput('rearrage');
113 | });
114 | },
115 | rearrage: function () {
116 | var $e = $(this);
117 | var settings = $e.data('mdmTabularInput').settings;
118 | var no = 1;
119 | element($e, settings.container).children(settings.itemSelector).each(function () {
120 | $(this).find(settings.serialSelector).text(no++);
121 | });
122 | $e.trigger(events.change);
123 | },
124 | addRow: function (values) {
125 | var $e = $(this);
126 | var settings = $e.data('mdmTabularInput').settings;
127 | var counter = settings.counter++;
128 | var template = replace(settings.template, settings.replaces, counter);
129 | var $row = $(template);
130 |
131 | var event = $.Event(events.beforeAdd);
132 | $e.trigger(event, [$row]);
133 | if (event.result !== false) {
134 | element($e, settings.container).append($row);
135 | if (values) {
136 | $row.find(':input[data-field]').each(function (){
137 | var $input = $(this);
138 | var field = $input.data('field');
139 | if (values[field] !== undefined) {
140 | $input.val(values[field]);
141 | }
142 | });
143 | }
144 | $e.trigger(events.afterAdd, [$row]);
145 | // add js
146 | if (settings.templateJs) {
147 | evaluateJs(replace(settings.templateJs, settings.replaces, counter));
148 | }
149 | // validation for active form
150 | if (settings.formSelector && settings.validations && settings.validations.length) {
151 | var $form = $(settings.formSelector);
152 | var validations = $.extend(true, {}, settings.validations);
153 | $.each(validations, function () {
154 | var validation = this;
155 | $.each(['id', 'name', 'container', 'input'], function () {
156 | validation[this] = replace(validation[this], settings.replaces, counter);
157 | });
158 | $form.yiiActiveForm('add', validation);
159 | });
160 | }
161 | $e.mdmTabularInput('rearrage');
162 | }
163 | return $row;
164 | },
165 | deleteRow: function ($row) {
166 | var $e = $(this);
167 | var settings = $e.data('mdmTabularInput').settings;
168 | if (!$row instanceof jQuery) {
169 | $row = element($e, settings.container).children(settings.itemSelector).eq($row);
170 | }
171 |
172 | var event = $.Event(events.beforeDelete);
173 | $e.trigger(event, [$row]);
174 | if (event.result !== false) {
175 | var vals = {};
176 | for (var k in settings.replaces) {
177 | vals[k] = $row.data(k);
178 | }
179 | $row.remove();
180 | $e.trigger(events.afterDelete);
181 | if (settings.formSelector && settings.validations && settings.validations.length) {
182 | var $form = $(settings.formSelector);
183 | $.each(settings.validations, function () {
184 | if (this.id) {
185 | var sid = replace(this.id, settings.replaces, vals);
186 | $form.yiiActiveForm('remove', sid);
187 | }
188 | });
189 | }
190 | $e.mdmTabularInput('rearrage');
191 | }
192 | },
193 | getSelectedRows: function () {
194 | var $e = $(this);
195 | var settings = $e.data('mdmTabularInput').settings;
196 | var rows = [];
197 | element($e, settings.container).children(settings.itemSelector)
198 | .filter('.selected').each(function () {
199 | rows.push($(this));
200 | });
201 | return rows;
202 | },
203 | getSelectedRow: function () {
204 | var $e = $(this);
205 | var settings = $e.data('mdmTabularInput').settings;
206 | return element($e, settings.container).children(settings.itemSelector)
207 | .filter('.selected').first();
208 | },
209 | getAllRows: function () {
210 | var $e = $(this);
211 | var settings = $e.data('mdmTabularInput').settings;
212 | var rows = [];
213 | element($e, settings.container).children(settings.itemSelector).each(function () {
214 | rows.push($(this));
215 | });
216 | return rows;
217 | },
218 | getValues: function () {
219 | var $e = $(this);
220 | var settings = $e.data('mdmTabularInput').settings;
221 | var values = [];
222 | element($e, settings.container).children(settings.itemSelector).each(function () {
223 | var value = {};
224 | $(this).find(':input[data-field]').each(function () {
225 | value[$(this).data('field')] = $(this).val();
226 | });
227 | values.push(value);
228 | });
229 | return values;
230 | },
231 | getValue: function ($row) {
232 | var $e = $(this);
233 | var settings = $e.data('mdmTabularInput').settings;
234 | if (!$row instanceof jQuery) {
235 | $row = element($e, settings.container).children(settings.itemSelector).eq($row);
236 | }
237 |
238 | var value = {};
239 | $row.find(':input[data-field]').each(function () {
240 | value[$(this).data('field')] = $(this).val();
241 | });
242 | return value;
243 | },
244 | getCount: function () {
245 | var $e = $(this);
246 | var settings = $e.data('mdmTabularInput').settings;
247 | return element($e, settings.container).children(settings.itemSelector).length;
248 | },
249 | toggleSelectRow: function ($row) {
250 | var $e = $(this);
251 | var settings = $e.data('mdmTabularInput').settings;
252 | if (!settings.multiSelect) {
253 | var has = $row.hasClass('selected');
254 | element($e, settings.container).children(settings.itemSelector).removeClass('selected');
255 | if (!has) {
256 | $row.addClass('selected');
257 | }
258 | } else {
259 | $row.toggleClass('selected');
260 | }
261 | },
262 | selectRow: function ($row) {
263 | var $e = $(this);
264 | var settings = $e.data('mdmTabularInput').settings;
265 | if (!settings.multiSelect) {
266 | element($e, settings.container).children(settings.itemSelector).removeClass('selected');
267 | }
268 | $row.addClass('selected');
269 | },
270 | destroy: function () {
271 | return this.each(function () {
272 | $(this).unbind('.mdmTabularInput');
273 | $(this).removeData('mdmTabularInput');
274 | });
275 | },
276 | data: function () {
277 | return $(this).data('mdmTabularInput');
278 | }
279 | };
280 | })(window.jQuery);
281 |
--------------------------------------------------------------------------------