├── ActionColumn.php ├── BaseObect.php ├── ButtonColumn.php ├── Column.php ├── CropAsset.php ├── CropImage.php ├── DataColumn.php ├── GridInput.php ├── LICENSE ├── ModelHelper.php ├── MultipleTrait.php ├── README.md ├── SerialColumn.php ├── TabularAsset.php ├── TabularInput.php ├── TabularWidget.php ├── assets ├── css │ └── tabularInput.css └── js │ ├── dcropbox.js │ └── tabularInput.js ├── bower.json ├── composer.json └── views └── crop-image.php /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 | -------------------------------------------------------------------------------- /BaseObect.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 1.3 14 | */ 15 | class BaseObect extends \yii\base\BaseObject 16 | { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | '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 | field($model,"[$key]product_id")->textInput()->label(false); ?> 59 | field($model,"[$key]qty")->textInput()->label(false); ?> 60 | 61 | ``` 62 | 63 | # GridInput Widget 64 | ```php 65 | '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 | ``` -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | '<@@' => ' '' => '?>', 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | })(); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /views/crop-image.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
> 10 |
11 |
12 | 13 | 14 | 15 | 16 |
17 | 18 |
--------------------------------------------------------------------------------