├── .gitbook.yaml
├── .github
└── workflows
│ └── main.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer.json
├── docs
├── clonning.md
├── configuration.md
├── getting-started.md
├── icons.md
├── index.md
├── installation.md
├── javascript-events.md
├── renderers.md
├── tips-and-tricks.md
└── usage.md
├── mkdocs.yml
└── src
├── MultipleInput.php
├── MultipleInputColumn.php
├── TabularColumn.php
├── TabularInput.php
├── assets
├── FontAwesomeAsset.php
├── MultipleInputAsset.php
├── MultipleInputSortableAsset.php
└── src
│ ├── css
│ ├── multiple-input.css
│ ├── multiple-input.min.css
│ ├── sorting.css
│ └── sorting.min.css
│ └── js
│ ├── jquery.multipleInput.js
│ ├── jquery.multipleInput.min.js
│ ├── sortable.js
│ └── sortable.min.js
├── components
├── BaseColumn.php
└── ValuePreparer.php
└── renderers
├── BaseRenderer.php
├── DivRenderer.php
├── ListRenderer.php
├── RendererInterface.php
└── TableRenderer.php
/.gitbook.yaml:
--------------------------------------------------------------------------------
1 | root: ./docs/
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Publish docs via GitHub Pages
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | build:
9 | name: Deploy docs
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout master
13 | uses: actions/checkout@v1
14 |
15 | - name: Deploy docs
16 | uses: mhausenblas/mkdocs-deploy-gh-pages@master
17 | env:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii2 multiple input change log
2 | ==============================
3 |
4 | 2.30.0 (in development)
5 | =======================
6 | - #363 fix rendering header and footer in case of using POS_ROWS_BEGIN in TableRenderer (unclead)
7 |
8 | 2.30.0
9 | =======================
10 | - #369 fix rendering action buttons in case the dataset contains non-numeric indices (unclead)
11 |
12 | 2.29.0
13 | =======================
14 | - fix addind active form fields doesn't work properly in case of 10 rows and more (unclead)
15 | - revert changes in normalize method because it affected ajax validation
16 | - fix addind active form fields in nested columns (unclead)
17 |
18 | 2.28.0
19 | =======================
20 | - replace outdated jquery-sortable.js with a modern alternative (sortable.js) (sankam-nikolya)
21 |
22 | 2.27.0
23 | ======
24 | - #367 (fix) ajax validation doesn't work for newly added/cloned inputs
25 |
26 | 2.26.2
27 | ======
28 | - prevent error loop in case of undefined $wrapper.data('multipleInput') (cebe)
29 |
30 | 2.26.1
31 | ======
32 | - remove version from composer.json
33 |
34 | 2.26.0
35 | ======
36 | - fix calculation of the current row index
37 | - fix incrementing the current row index after adding a new row
38 |
39 | 2.25.0
40 | ======
41 | - rework cloning: fix #277, #351, #348 (unclead)
42 |
43 | 2.24.0
44 | ======
45 | - #339 don't set tabindex explicitly
46 |
47 | 2.23.0
48 | ======
49 | - always use `id` from the settings if it is specified
50 | - Ability to add custom tabindex via options array
51 | - #335 fix input name in case of one column and enabled sorting
52 |
53 | 2.22.0
54 | ======
55 | - Ignore dev files in zip distribution (sup-ham)
56 | - #292 Fixed tests for last PHPUnit
57 | - Added support prepare values of attributes with same name as the relation
58 |
59 | 2.21.4
60 | ======
61 | - Fix replaceAll unfinished modify
62 | - More completely type detect to replaceAll
63 |
64 | 2.21.3
65 | ======
66 | - fix retrieving AR relation data
67 |
68 | 2.21.2
69 | ======
70 | - FIX wrapper options for bootstrap theme
71 |
72 | 2.21.1
73 | ======
74 | - #286 avoid removal of all rows on the page when there are several widgets on the page and method `clear` was called
75 |
76 | 2.21.0
77 | ======
78 | - #279 ability to prepend new row instead of append
79 |
80 | 2.20.6
81 | ======
82 |
83 | - don't cast JsExpression to string after replace widget placeholder
84 |
85 | 2.20.0
86 | ======
87 |
88 | - #278 allow zero name
89 | - #261 replace the widget placeholder in all nested options
90 |
91 | 2.19.0
92 | ======
93 | - add template for input (bscheshirwork)
94 | - pass more params to a prepareValue closure (bscheshirwork)
95 | - add DivRenderer (bscheshirwork)
96 |
97 | 2.18.0
98 | ======
99 | - #246 accept `\Traversable` in model attribute for `yield` compatibility (bscheshirwork)
100 | - #250 accept `\Traversable` in TableRenderer and ListRenderer for `yield` compatibility (bscheshirwork)
101 | - #253 allow to omit a name for static column
102 | - #257 added `jsPositions` property for the `BaseRenderer` to set right order js-code in `jsInit` and `jsTemplates` (Spell6inder)
103 | - #259 added `columnOptions` property in the `BaseColumn` for TableRenderer and ListRenderer to support HTML options of individual column (InsaneSkull)
104 |
105 | 2.17.0
106 | ======
107 | - #215 collect all js script that has to be evaluate when add new row (not only from " on ready" section)
108 | - #198 introduce the option `theme` to disable all bootstrap css classes
109 | - #197 explicitly set tabindex for all inputs
110 | - #175 option `showGeneralError` to enable displaying of general error message
111 |
112 | 2.16.0
113 | ======
114 | - #220 fixed error message for clientValidation and ajaxValidation (antkaz)
115 | - #228 added `iconMap` and `iconSource`property for MultipleInput and TabularInput
116 | - #228 changed the following methods to support icon class:
117 | BaseColumn->renderDragColumn(), TableRenderer->renderCellContent(), BaseRenderer->prepareButtons()
118 | - #194 added support of yii\base\DynamicModel
119 | - #186 added event `afterDropRow`
120 |
121 | 2.15.0
122 | =======================
123 | - #217 added `layoutConfig` property for the ListRenderer (antkaz)
124 |
125 | 2.14.0
126 | ======
127 | - #202 added extra buttons (dimmitri)
128 | - PR#201 added optional clone button (alex-nesterov)
129 |
130 | 2.13.0
131 | ======
132 | - #152 added ability to allow an empty list (or set `min` property to 0) for TabularInput
133 |
134 | 2.12.0
135 | ======
136 | - Rename yii\base\Object to yii\base\BaseObject
137 |
138 |
139 | 2.11.0
140 | ======
141 | - Added the possibility to substitute buttons before rows
142 |
143 | 2.10.0
144 | ======
145 | - #170: Added global options `enableError`
146 | - #154: Added missing js event: beforeAddRow
147 |
148 | 2.9.0
149 | =====
150 |
151 | - Pass the added row to `afterAddRow` event
152 |
153 | 2.8.2
154 | =====
155 |
156 | - Fixed conflict with jQuery UI sortable
157 |
158 | 2.8.1
159 | =====
160 |
161 | - Fixed client validation
162 |
163 | 2.8.0
164 | =====
165 |
166 | - #137: added option `nameSuffix` to avoid errors related to duplication of id in case when you use several copies of the widget on a page
167 |
168 | 2.7.1
169 | =====
170 |
171 | - Fixed assets
172 |
173 | 2.7.0
174 | =====
175 |
176 | - Fixed an incorrect behavior of widget in case of ajax loading (e.g. in modal window)
177 |
178 | 2.6.1
179 | =====
180 |
181 | - Fixed assets
182 |
183 | 2.6.0
184 | =====
185 |
186 | - PR#132: Implemented `Sortting` (sankam-nikolya)
187 | - PR#132: fixed if attribute is set and hasProperty return false (sankam-nikolya)
188 |
189 | 2.5.0
190 | =====
191 |
192 | - #127: fixed js actions
193 |
194 | 2.4.0
195 | =====
196 |
197 | - Implemented `ListRenderer`
198 |
199 | 2.3.1
200 | =====
201 |
202 | - Fixed ajax validation for embedded fields
203 |
204 | 2.3.0
205 | =====
206 |
207 | - #107: render a hidden input when `MultipleInput` is used for active field
208 | - #109: respect ID when using a widget's placeholder
209 |
210 | 2.2.0
211 | =====
212 |
213 | - #104: Fixed preparation of js attributes (Choate, unclead)
214 | - Fixed removal of row with index 0 via js api method (pvlg)
215 |
216 |
217 | 2.1.1
218 | =====
219 |
220 | - Enh: Passing a deleted row to the event
221 |
222 | 2.1.0
223 | =====
224 |
225 | - Enh #37: Support of client validation
226 |
227 | 2.0.1
228 | =====
229 |
230 | - Bug #105: Change vendor name in namespace from yii to unclead to respect Yii recommendations
231 |
232 | 2.0.0
233 | =====
234 |
235 | - Renamed `limit` option to `max`
236 | - Changed namespace from `unclead\widgets` to `yii\multipleinput`
237 | - #92: Adjustments for correct work with AR relations
238 | - Enh #104: Added method to set value of an particular option
239 |
240 | 1.4.1
241 | =====
242 |
243 | - #99: Respect "defaultValue" if it is set and current value is empty (unclead)
244 |
245 | 1.4.0
246 | -----
247 |
248 | - #94: added ability to set custom renderer (unclead, bokodi-dev)
249 | - #97: Respect `addButtonPosition` when rendering the button (unclead)
250 |
251 | 1.3.1
252 | -----
253 |
254 | - Bug: Use method `::className` instead of `::class`
255 |
256 | 1.3.0
257 | -----
258 |
259 | - #79 Added support for embedded MultipleInput widget (unclead, execut)
260 | - Enh: Added ability to render `add` button in the footer (unclead)
261 | - Enh: Improving for better work without ActiveForm (unclead)
262 | - Enh: Added ability to render `add` button at several positions (unclead)
263 |
264 | 1.2.19
265 | ------
266 |
267 | - #85: fixed `$enableError` not render element in template (thiagotalma)
268 |
269 | 1.2.18
270 | ------
271 |
272 | - #81 fixed output of errors in case of non-ajax validation
273 |
274 | 1.2.17
275 | ------
276 |
277 | - Enh: increased default value for the property `limit` (ivansal)
278 | - Enh: Added support of associative array in data (ivansal)
279 | - Bug: fixed double execution events for included MultipleInput (fiamma06)
280 |
281 | 1.2.16
282 | ------
283 |
284 | - Bug #70: replacing of the placeholder after preparing the content of row
285 |
286 | 1.2.15
287 | ------
288 |
289 | - Added note about usage widget with ajax
290 |
291 | 1.2.14
292 | ------
293 |
294 | - Bug #71: trigger the event after actual removal of row
295 |
296 | 1.2.13
297 | ------
298 |
299 | - Added new js events (add/remove/clear inputs) and integrated the gulp for minification of assets (veksa)
300 | - Added support of closure for parameter `options` (veksa)
301 |
302 | 1.2.12
303 | ------
304 |
305 | - Hotfix: Fixed error when array_key_exits (kongoon)
306 |
307 | 1.2.11
308 | ------
309 |
310 | - Bug #61: Fixed a rendering of remove button
311 | - Bug #62: Incorrect behavior is case when `min` is equal to `limit`
312 | - Bug #64: Radio/checkbox lists doesn't work correctly
313 |
314 | 1.2.10
315 | ------
316 |
317 | - Enh #59 Added columnClass property (unclead)
318 |
319 | 1.2.9
320 | -----
321 |
322 | - Enh #56: add `rowOptions` property
323 |
324 | 1.2.8
325 | -----
326 |
327 | - Enh: Don't show action column when limit is `equal` to `min`
328 |
329 | 1.2.7
330 | -----
331 |
332 | - Bug #55: Attach click events to the widget wrapper instead of `$(document)`
333 |
334 | 1.2.6
335 | -----
336 |
337 | - Bug #49: urlencoded field token replacement in js template (rolmonk)
338 | - Enh #48: Added option `min` for setting minimum number of rows
339 | - Enh: added option `addButtonPosition`
340 |
341 | 1.2.5
342 | -----
343 |
344 | - Bug #46: Renamed placeholder to avoid conflict with other plugins
345 | - Bug #47: Use Html helper for rendering buttons instead of Button widget
346 | - Enh: Deleted yii2-bootstrap dependency
347 |
348 | 1.2.4
349 | -----
350 |
351 | - Bug #39: TabularInput: now new row does't copy values from the most recent row
352 | - Enh #40: Pass the current row for removal when calling `beforeDeleteRow` event
353 |
354 |
355 | 1.2.3
356 | -----
357 |
358 | - Enh #34: Added option `allowEmptyList` (unclead)
359 | - Enh #35: Added option `enableGuessTitle` for MultipleInput (unclead)
360 | - Bug #36: Use PCRE_MULTILINE modifier in regex
361 |
362 | 1.2.2
363 | -----
364 |
365 | - Enh #31: Added support of anonymous function for `items` attribute (unclead, stepancher)
366 | - Enh: added hidden field for radio and checkbox inputs (unclead, kotchuprik)
367 | - Enh: improved css (fiamma06)
368 |
369 | 1.2.1
370 | -----
371 |
372 | - Bug #25 fixed rendering when data is empty
373 | - Bug #27 fixed element's prefix generation
374 |
375 | 1.2.0
376 | -----
377 |
378 | - Bug #19 Refactoring rendering of inputs (unclead)
379 | - Bug #20 Added hasAttribute checking for AR models (unclead)
380 | - Enh #22 Added `TabularInput` widget (unclead), rendering logic has been moved to separate class (renderer)
381 |
382 | 1.1.0
383 | -----
384 |
385 | - Bug #17: display inline errors (unclead, mikbox74)
386 | - Enh #11: Improve js events (unclead)
387 | - Bug #16: correct use of defaultValue property (unclead)
388 | - code improvements (unclead)
389 |
390 | 1.0.4
391 | --------------------
392 |
393 | - Bug #15: Fix setting current values of dropDownList (unclead)
394 | - Bug #16: fix render of dropDown and similar inputs (unclead)
395 | - Enh: Add attributeOptions property
396 |
397 | 1.0.3
398 | -----
399 | - Bug: Hidden fields no longer break markup (unclead, kotchuprik)
400 |
401 | 1.0.2
402 | -----
403 |
404 | - Enh: added minified version of js script (unclead)
405 | - Enh #8: renamed placeholders for avoid conflicts with other widgets (unclead)
406 | - Enh #7: customization of header cell
407 |
408 | 1.0.1
409 | -----
410 |
411 | - Enh #1: Implemented ability to use widget as column type (unclead)
412 | - Enh: add js events (ZAYEC77)
413 |
414 | 1.0.0
415 | -----
416 |
417 | first stable release
418 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Eugene Tupikov
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 |
9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10 |
11 | * Neither the names of Eugene Tupikov or unclead nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Yii2 Multiple input widget.
2 | Yii2 widget for handle multiple inputs for an attribute of model and tabular input for batch of models.
3 |
4 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
5 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
6 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
7 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
8 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
9 |
10 | ## Latest release
11 | The latest stable version of the extension is v2.27.0 Follow the [instruction](./UPGRADE.md) for upgrading from previous versions
12 |
13 | ## Installation
14 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
15 |
16 | Either run
17 |
18 | ```
19 | php composer.phar require unclead/yii2-multiple-input "~2.0"
20 | ```
21 |
22 | or add
23 |
24 | ```
25 | "unclead/yii2-multiple-input": "~2.0"
26 | ```
27 |
28 | to the require section of your `composer.json` file.
29 |
30 | ## Basic usage
31 |
32 | 
33 |
34 | For example you want to have an ability of entering several emails of user on profile page.
35 | In this case you can use yii2-multiple-input widget like in the following code
36 |
37 | ```php
38 | use unclead\multipleinput\MultipleInput;
39 |
40 | ...
41 |
42 | field($model, 'emails')->widget(MultipleInput::className(), [
44 | 'max' => 6,
45 | 'min' => 2, // should be at least 2 rows
46 | 'allowEmptyList' => false,
47 | 'enableGuessTitle' => true,
48 | 'addButtonPosition' => MultipleInput::POS_HEADER, // show add button in the header
49 | ])
50 | ->label(false);
51 | ?>
52 | ```
53 |
54 | ## Documentation
55 |
56 | You can find a full version of documentation [here](https://unclead.github.io/yii2-multiple-input/)
57 |
58 | ## License
59 |
60 | **yii2-multiple-input** is released under the BSD 3-Clause License. See the bundled [LICENSE.md](./LICENSE.md) for details.
61 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | Upgrading Instructions for yii2-multiple-widget
2 | ===============================================
3 |
4 | !!!IMPORTANT!!!
5 |
6 | The following upgrading instructions are cumulative. That is,
7 | if you want to upgrade from version A to version C and there is
8 | version B between A and C, you need to following the instructions
9 | for both A and B.
10 |
11 | Upgrade 2.12.0
12 | --------------
13 | - Ensure you use yii2 2.0.13 and higher, otherwise you have to use previous version of the widget
14 |
15 | Upgrade from 2.2.0 tp 2.3.0
16 | ---------------------------
17 |
18 | - Ensure that you set `id` option in case you are using js actions, otherwise your old code won't work.
19 |
20 | Upgrade from 2.0.0 tp 2.0.1
21 | ---------------------------
22 |
23 | - Change namespace prefix `yii\multipleinput\` to `unclead\multipleinput\`.
24 |
25 | Upgrade from 1.4 to 2.0
26 | -----------------------
27 |
28 | - Rename option `limit` to `max`
29 | - Change namespace prefix `unclead\widgets\` to `yii\multipleinput\`.
30 |
31 | Upgrade from 1.3 to 1.4
32 | -----------------------
33 | - In scope of #97 was changed a behavior of rendering add button. The button renders now depends on option `addButtonPosition` and now this
34 | option is not set by default.
35 |
36 |
37 | Upgrade from 1.2 to 1.3
38 | -----------------------
39 |
40 | - The mechanism of customization configuration by using index placeholder was changed in scope of implementing support of nested `MultipleInput`
41 | If you customize configuration by using index placeholder you have to add ID of widget to the placeholder.
42 | For example, `multiple_index` became `multiple_index_question_list`
43 |
44 |
45 | Upgrade from version less then 1.1.0
46 | ------------------------------------
47 |
48 | After installing version 1.1.0 you have to rename js events following the next schema:
49 |
50 | - Event `init` rename to `afterInit`
51 | - Event `addNewRow` rename to `afterAddRow`
52 | - Event `removeRow` rename to `afterDeleteRow`
53 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unclead/yii2-multiple-input",
3 | "description": "Widget for handle multiple inputs for an attribute of Yii2 framework model",
4 | "keywords": [
5 | "yii2",
6 | "yii2 multiple input",
7 | "yii2 array input",
8 | "yii2 multiple field",
9 | "yii2 tabular input"
10 | ],
11 | "type": "yii2-extension",
12 | "license": "BSD-3-Clause",
13 | "support": {
14 | "issues": "https://github.com/unclead/yii2-multiple-input/issues?state=open",
15 | "source": "https://github.com/unclead/yii2-multiple-input"
16 | },
17 | "authors": [
18 | {
19 | "name": "Eugene Tupikov",
20 | "email": "unclead.nsk@gmail.com"
21 | }
22 | ],
23 | "require": {
24 | "php": ">=5.4.0",
25 | "yiisoft/yii2": ">=2.0.38"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "5.7.*"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "unclead\\multipleinput\\examples\\": "examples/",
33 | "unclead\\multipleinput\\": "src/",
34 | "unclead\\multipleinput\\tests\\": "tests/"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/clonning.md:
--------------------------------------------------------------------------------
1 | # Clonning
2 |
3 | 
4 |
5 | ```text
6 | use unclead\multipleinput\MultipleInput;
7 |
8 | ...
9 |
10 | $form->field($model, 'products')->widget(MultipleInput::className(), [
11 | 'max' => 10,
12 | 'cloneButton' => true,
13 | 'columns' => [
14 | [
15 | 'name' => 'product_id',
16 | 'type' => 'dropDownList',
17 | 'title' => 'Special Products',
18 | 'defaultValue' => 1,
19 | 'items' => [
20 | 1 => 'id: 1, price: $19.99, title: product1',
21 | 2 => 'id: 2, price: $29.99, title: product2',
22 | 3 => 'id: 3, price: $39.99, title: product3',
23 | 4 => 'id: 4, price: $49.99, title: product4',
24 | 5 => 'id: 5, price: $59.99, title: product5',
25 | ],
26 | ],
27 | [
28 | 'name' => 'time',
29 | 'type' => DateTimePicker::className(),
30 | 'title' => 'due date',
31 | 'defaultValue' => date('d-m-Y h:i')
32 | ],
33 | [
34 | 'name' => 'count',
35 | 'title' => 'Count',
36 | 'defaultValue' => 1,
37 | 'enableError' => true,
38 | 'options' => [
39 | 'type' => 'number',
40 | 'class' => 'input-priority',
41 | ]
42 | ]
43 | ]
44 | ])->label(false);
45 | ```
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Widget support the following options that are additionally recognized over and above the configuration options in the InputWidget.
4 |
5 | ## Base options
6 |
7 | **theme** _string_: specify the theme of the widget. Available 2 themes:
8 |
9 | * `default` with only widget css classes
10 | * `bs` \(twitter bootstrap\) theme with additional BS ccs classes\).
11 |
12 | Default value is `bs`
13 |
14 | **max** _integer_: maximum number of rows. If not set will default to unlimited
15 |
16 | **min** _integer_: minimum number of rows. Set to `0` if you need the empty list in case you don't have any data
17 |
18 | **prepend** _boolean_: add a new row to the beginning of the list, not to the end
19 |
20 | **attributeOptions** _array_: client-side attribute options, e.g. enableAjaxValidation. You may use this property in case when you use widget without a model, since in this case widget is not able to detect client-side options automatically
21 |
22 | **addButtonPosition** _integer\|array_: the position\(s\) of `add` button. This can be `MultipleInput::POS_HEADER`, `MultipleInput::POS_ROW`, `MultipleInput::POS_ROW_BEGIN` or `MultipleInput::POS_FOOTER`.
23 |
24 | **addButtonOptions** _array_: the HTML options for `add` button. Can contains `class` and `label` keys
25 |
26 | **removeButtonOptions** _array_: the HTML options for `remove` button. Can contains `class` and `label` keys
27 |
28 | **cloneButton** _bool_: whether need to enable clone buttons or not
29 |
30 | **cloneButtonOptions** _array_: the HTML options for `remove` button. Can contains `class` and `label` keys
31 |
32 | **data** _array_: array of values in case you use widget without model
33 |
34 | **models** _array_: the list of models. Required in case you use `TabularInput` widget
35 |
36 | **allowEmptyList** _boolean_: whether to allow the empty list. **Deprecateed** use the `min` option instead
37 |
38 | **columnClass** _string_: the name of column class. You can specify your own class to extend base functionality. Defaults to `unclead\multipleinput\MultipleInputColumn` for `MultipleInput` and `unclead\multipleinput\TabularColumn` for `TabularInput`.
39 |
40 | **rendererClass** _string_: the name of renderer class. You can specify your own class to extend base functionality. Defaults to `unclead\multipleinput\renderers\TableRenderer`.
41 |
42 | **columns** _array_: the row columns configuration where you can set the properties which is described below
43 |
44 | **rowOptions** _array\|\Closure_: the HTML attributes for the table body rows. This can be either an array specifying the common HTML attributes for all body rows, or an anonymous function that returns an array of the HTML attributes. It should have the following signature:
45 |
46 | ```php
47 | function ($model, $index, $context)
48 | ```
49 |
50 | * `$model`: the current data model being rendered
51 | * `$index`: the zero-based index of the data model in the model array
52 | * `$context`: the widget object
53 |
54 | **sortable** _bool_: whether need to enable sorting or not
55 |
56 | **modelClass** _string_: a class of model which is used to render `TabularInput`. You must specify this property when a list of `models` is empty. If this property is not specified the widget will detect it based on a class of `models`
57 |
58 | **extraButtons** _string\|\Closure_: the HTML content that will be rendered after the buttons. It can be either string or an anonymous function that returns a string which will be treated as HTML content. It should have the following signature:
59 |
60 | ```php
61 | function ($model, $index, $context)
62 | ```
63 |
64 | * `$model`: the current data model being rendered
65 | * `$index`: the zero-based index of the data model in the model array
66 | * `$context`: the MultipleInput widget object
67 |
68 | **layoutConfig** _array_: CSS grid classes for horizontal layout \(only supported for `ListRenderer` class\). This must be an array with these keys:
69 |
70 | * `'offsetClass'`: the offset grid class to append to the wrapper if no label is rendered
71 | * `'labelClass'`: the label grid class
72 | * `'wrapperClass'`: the wrapper grid class
73 | * `'errorClass'`: the error grid class
74 |
75 | **showGeneralError** _bool_: whether need to show error message for main attribute, when you don't want to validate particular input and want to validate a filed in general.
76 |
77 | ## Column options
78 |
79 | **name** _string_: input name. _Required options_
80 |
81 | **type** _string_: type of the input. If not set will default to `textInput`. Read more about the types described below
82 |
83 | **title** _string_: the column title
84 |
85 | **value** _Closure_: you can set it to an anonymous function with the following signature:
86 |
87 | ```php
88 | function($data) {}
89 | ```
90 |
91 | **defaultValue** _string_: default value of input
92 |
93 | **items** _array_\|_Closure_: the items for input with type dropDownList, listBox, checkboxList, radioList or anonymous function which return array of items and has the following signature:
94 |
95 | ```php
96 | function($data) {}
97 | ```
98 |
99 | **options** _array_\|_Closure_: the HTML attributes for the input, you can set it as array or an anonymous function with the following signature:
100 |
101 | ```php
102 | function($data) {}
103 | ```
104 |
105 | **headerOptions** _array_: the HTML attributes for the header cell
106 |
107 | **enableError** _boolean_: whether to render inline error for the input. Default to `false`
108 |
109 | **errorOptions** _array_: the HTMl attributes for the error tag
110 |
111 | **nameSuffix** _string_: the unique prefix for attribute's name to avoid id duplication e.g. in case of using several copies of the widget on a page and one column is a Select2 widget
112 |
113 | **tabindex** _integer_: use it to customize a form element `tabindex`
114 |
115 | **attributeOptions** _array_: client-side options of the attribute, e.g. enableAjaxValidation. You can use this property for custom configuration of the column (attribute). By default, the column will use options which are defined on widget level.
116 |
117 | _Supported versions >= 2.1.0
118 |
119 | **columnOptions** _array|\Closure_: the HTML attributes for the indivdual table body column. This can be either an array specifying the common HTML attributes for indivdual body column, or an anonymous function that returns an array of the HTML attributes.
120 |
121 | It should have the following signature:
122 | ```php
123 | function ($model, $index, $context)
124 | ```
125 | * `$model`: the current data model being rendered
126 | * `$index`: the zero-based index of the data model in the model array
127 | * `$context`: the widget object
128 |
129 | _Supported versions >= 2.18.0_
130 |
131 | **inputTemplate** _string_: the template of input for customize view. Default is `{input}`.
132 |
133 | **Example**
134 |
135 | `
{input}
`
136 |
137 | ## Input types
138 |
139 | Each column in a row can has their own type. Widget supports:
140 |
141 | * all yii2 html input types:
142 | * `textInput`
143 | * `dropDownList`
144 | * `radioList`
145 | * `textarea`
146 | * For more detail look at [Html helper class](http://www.yiiframework.com/doc-2.0/yii-helpers-html.html)
147 | * input widget \(widget that extends from `InputWidget` class\). For example, `yii\widgets\MaskedInput`
148 | * `static` to output a static HTML content
149 |
150 | For using widget as column input you may use the following code:
151 |
152 | ```php
153 | echo $form->field($model, 'phones')->widget(MultipleInput::className(), [
154 | ...
155 | 'columns' => [
156 | ...
157 | [
158 | 'name' => 'phones',
159 | 'title' => $model->getAttributeLabel('phones'),
160 | 'type' => \yii\widgets\MaskedInput::className(),
161 | 'options' => [
162 | 'class' => 'input-phone',
163 | 'mask' => '999-999-99-99',
164 | ],
165 | ],
166 | ],
167 | ])->label(false);
168 | ```
169 |
170 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | I found this small guide here [https://stackoverflow.com/a/51849747](https://stackoverflow.com/a/51849747) and I think it is a good example of basic usage of the widget
4 |
5 | ## Question
6 |
7 | I want to generate a different number of rows with values from my database. How can I do this?
8 |
9 | I can design my columns in view and edit data manually after a page was generated. But miss how to program the number of rows and their values in the view.
10 |
11 | My code is as follows:
12 |
13 | ```text
14 | = $form->field($User, 'User')->widget(MultipleInput::className(), [
15 | 'min' => 0,
16 | 'max' => 4,
17 | 'columns' => [
18 | [
19 | 'name' => 'name',
20 | 'title' => 'Name',
21 | 'type' => 'textInput',
22 | 'options' => [
23 | 'onchange' => $onchange,
24 | ],
25 | ],
26 | [
27 | 'name' => 'birth',
28 | 'type' => \kartik\date\DatePicker::className(),
29 | 'title' => 'Birth',
30 | 'value' => function($data) {
31 | return $data['day'];
32 | },
33 |
34 | 'options' => [
35 | 'pluginOptions' => [
36 | 'format' => 'dd.mm.yyyy',
37 | 'todayHighlight' => true
38 | ]
39 | ]
40 | ],
41 |
42 | ]
43 | ])->label(false);
44 | ```
45 |
46 | How can I make \(for example\) 8 rows with different values, and also have the ability to edit/remove/update some of them?
47 |
48 | ## Answer
49 |
50 | You need to look into the documentation as it says that you need to assign a separate field into the model which will store all the schedule in form of JSON and then provide it back to the field when editing/updating the model.
51 |
52 | You have not added the appropriate model to verify how are you creating the field User in your given case above. so, I will try to create a simple example that will help you implement it in your scenario.
53 |
54 | For Example.
55 |
56 | You have to store a user in the database along with his favorite books.
57 |
58 | ```text
59 | User
60 | id, name, email
61 |
62 | Books
63 | id, name
64 | ```
65 |
66 | Create a field/column in your User table with the name schedule of type text, you can write a migration or add manually. Add it to the rules in the User model as safe.
67 |
68 | like below
69 |
70 | ```text
71 | public function rules() {
72 | return [
73 | ....//other rules
74 | [ [ 'schedule'] , 'safe' ]
75 | ];
76 | }
77 | ```
78 |
79 | Add the widget to the newly created column in ActiveForm
80 |
81 | ```text
82 | echo $form->field($model,'schedule')->widget(MultipleInput::class,[
83 | 'max' => 4,
84 | 'columns' => [
85 | [
86 | 'name' => 'book_id',
87 | 'type' => 'dropDownList',
88 | 'title' => 'Book',
89 | 'items' => ArrayHelper::map( Books::find()->asArray()->all (),'id','name'),
90 | ],
91 | ]
92 |
93 | ]);
94 | ```
95 |
96 | When saving the User model convert the array to JSON string
97 |
98 | ```text
99 | if( Yii::$app->request->isPost && $model->load(Yii::$app->request->post()) ){
100 | $model->schedule = \yii\helpers\Json::encode($model->schedule);
101 | $model->save();
102 | }
103 | ```
104 |
105 | Override the afterFind\(\) of the User model to covert the JSON back to the array before loading the form
106 |
107 | ```text
108 | public function afterFind() {
109 | parent::afterFind();
110 | $this->schedule = \yii\helpers\Json::decode($this->schedule);
111 | }
112 | ```
113 |
114 | Now when saved the schedule field against the current user will have the JSON for the selected rows for the books, as many selected, for example, if I saved three books having ids\(1,2,3\) then it will have JSON
115 |
116 | ```text
117 | {
118 | "0": {
119 | "book_id": "1"
120 | },
121 | "2": {
122 | "book_id": "2"
123 | },
124 | "3": {
125 | "book_id": "3"
126 | }
127 | }
128 | ```
129 |
130 | The above JSON will be converted to an array in the afterFind\(\) so that the widget loads the saved schedule when you EDIT the record.
131 |
132 | Now go to your update page or edit the newly saved model you will see the books loaded automatically.
133 |
134 |
--------------------------------------------------------------------------------
/docs/icons.md:
--------------------------------------------------------------------------------
1 | # Using other icon libraries
2 |
3 | Multiple input and Tabular input widgets now support FontAwesome and indeed any other icon library you chose to integrate into your project.
4 |
5 | To take advantage of this, please proceed as follows:
6 |
7 | 1. Include the preferred icon library in your project. If you wish to use **font awesome**, you can use the included FontAwesomeAsset which will integrate the free fa from their CDN;
8 | 2. Add a mapping for your preferred icon library if it is not in the `iconMap` array of the widget, like the following;
9 |
10 | ```text
11 | public $iconMap = [
12 | 'glyphicons' => [
13 | 'drag-handle' => 'glyphicon glyphicon-menu-hamburger',
14 | 'remove' => 'glyphicon glyphicon-remove',
15 | 'add' => 'glyphicon glyphicon-plus',
16 | 'clone' => 'glyphicon glyphicon-duplicate',
17 | ],
18 | 'fa' => [
19 | 'drag-handle' => 'fa fa-bars',
20 | 'remove' => 'fa fa-times',
21 | 'add' => 'fa fa-plus',
22 | 'clone' => 'fa fa-files-o',
23 | ],
24 | 'my-amazing-icons' => [
25 | 'drag-handle' => 'my my-bars',
26 | 'remove' => 'my my-times',
27 | 'add' => 'my my-plus',
28 | 'clone' => 'my my-files',
29 | ]
30 | ];
31 | ```
32 |
33 | 3. Set the preferred icon source
34 |
35 | ```text
36 | public $iconSource = 'my-amazing-icons';
37 | ```
38 |
39 | If you do none of the above, the default behavior which assumes you are using `glyphicons` is retained.
40 |
41 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Yii2 Multiple input widget.
2 | Yii2 widget for handle multiple inputs for an attribute of model and tabular input for batch of models.
3 |
4 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
5 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
6 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
7 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
8 | [](https://packagist.org/packages/unclead/yii2-multiple-input)
9 |
10 | ## Latest release
11 | The latest stable version of the extension is v2.25.0 Follow the [instruction](./UPGRADE.md) for upgrading from previous versions
12 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
4 |
5 | Either run
6 |
7 | ```text
8 | php composer.phar require unclead/yii2-multiple-input "~2.0"
9 | ```
10 |
11 | or add
12 |
13 | ```text
14 | "unclead/yii2-multiple-input": "~2.0"
15 | ```
16 |
17 | to the `require`section of your `composer.json` file.
18 |
19 |
--------------------------------------------------------------------------------
/docs/javascript-events.md:
--------------------------------------------------------------------------------
1 | # Javascript events
2 |
3 | This widget has following events:
4 |
5 | * `afterInit`: triggered after initialization
6 | * `afterAddRow`: triggered after new row insertion
7 | * `beforeDeleteRow`: triggered before the row removal
8 | * `afterDeleteRow`: triggered after the row removal
9 | * `afterDropRow`: triggered after drop the row when sortable mode is on
10 |
11 | **Example**
12 |
13 | ```javascript
14 | jQuery('#multiple-input').on('afterInit', function(){
15 | console.log('calls on after initialization event');
16 | }).on('beforeAddRow', function(e, row, currentIndex) {
17 | console.log('calls on before add row event');
18 | }).on('afterAddRow', function(e, row, currentIndex) {
19 | console.log('calls on after add row event');
20 | }).on('beforeDeleteRow', function(e, row, currentIndex){
21 | // row - HTML container of the current row for removal.
22 | // For TableRenderer it is tr.multiple-input-list__item
23 | console.log('calls on before remove row event.');
24 | return confirm('Are you sure you want to delete row?')
25 | }).on('afterDeleteRow', function(e, row, currentIndex){
26 | console.log('calls on after remove row event');
27 | console.log(row);
28 | }).on('afterDropRow', function(e, item){
29 | console.log('calls on after drop row', item);
30 | });
31 | ```
32 |
33 | ## JavaScript operations
34 |
35 | ### add
36 |
37 | Adding new row with specified settings.
38 |
39 | Input arguments:
40 |
41 | * _object_ - values for inputs, can be filled with tags for dynamically added options for select \(for ajax select\).
42 |
43 | Example:
44 |
45 | ```javascript
46 | $('#multiple-input').multipleInput('add', {first: 10, second: ''});
47 | ```
48 |
49 | ### remove
50 |
51 | Remove row with specified index.
52 |
53 | Input arguments:
54 |
55 | * _integer_ - row number for removing, if not specified then removes last row.
56 |
57 | Example:
58 |
59 | ```javascript
60 | $('#multiple-input').multipleInput('remove', 2);
61 | ```
62 |
63 | ### clear
64 |
65 | Remove all rows
66 |
67 | ```javascript
68 | $('#multiple-input').multipleInput('clear');
69 | ```
70 |
71 | ### option
72 |
73 | Get or set a particular option
74 |
75 | Input arguments:
76 |
77 | * _string_ - a name of an option
78 | * _mixed_ - a value of an option \(optional\). If specified will be used as a new value of an option;
79 |
80 | Example:
81 |
82 | ```javascript
83 | $('#multiple-input').multipleInput('option', 'max');
84 | $('#multiple-input').multipleInput('option', 'max', 10);
85 | ```
86 |
87 |
--------------------------------------------------------------------------------
/docs/renderers.md:
--------------------------------------------------------------------------------
1 | # Renderers
2 |
3 | Currently widget supports three type of renderers
4 |
5 | ## TableRenderer
6 |
7 | 
8 |
9 | This renderer is enabled by default.
10 |
11 | ## ListRenderer
12 |
13 | 
14 |
15 | To enable this renderer you have to use an option `rendererClass`
16 |
17 | ```php
18 | field($model, 'schedule')->widget(MultipleInput::className(), [
20 | 'rendererClass' => \unclead\multipleinput\renderers\ListRenderer::className(),
21 | 'max' => 4,
22 | 'allowEmptyList' => true,
23 | 'rowOptions' => function($model) {
24 | $options = [];
25 |
26 | if ($model['priority'] > 1) {
27 | $options['class'] = 'danger';
28 | }
29 | return $options;
30 | },
31 | ```
32 |
33 | ## DivRenderer
34 |
35 | 
36 |
37 | To enable this renderer you have to use an option `rendererClass`
38 |
39 | ```php
40 | field($model, 'schedule')->widget(MultipleInput::className(), [
42 | 'rendererClass' => \unclead\multipleinput\renderers\ListRenderer::class,
43 | 'addButtonPosition' => MultipleInput::POS_ROW, // show add button inside of the row
44 | 'extraButtons' => function ($model, $index, $context) {
45 | if ($index === 0) {
46 | return Html::tag('div', Yii::t('object', 'Add object'), ['class' => 'mi-after-add']);
47 | }
48 |
49 | return Html::tag('div', Yii::t('object', 'Remove object'), ['class' => 'mi-after-remove']);
50 | },
51 | 'layoutConfig' => [
52 | 'offsetClass' => 'col-md-offset-2',
53 | 'labelClass' => 'col-md-2',
54 | 'wrapperClass' => 'col-md-6',
55 | 'errorClass' => 'col-md-offset-2 col-md-6',
56 | 'buttonActionClass' => 'col-md-offset-1 col-md-2',
57 | ],
58 | ...
59 | ```
60 |
61 |
--------------------------------------------------------------------------------
/docs/tips-and-tricks.md:
--------------------------------------------------------------------------------
1 | # Tips and tricks
2 |
3 | ## How to customize buttons
4 |
5 | You can customize `add` and `remove` buttons via `addButtonOptions` and `removeButtonOptions`. Here is a simple example of how you can use those options:
6 |
7 | ```php
8 | echo $form->field($model, 'emails')->widget(MultipleInput::className(), [
9 | 'max' => 5,
10 | 'addButtonOptions' => [
11 | 'class' => 'btn btn-success',
12 | 'label' => 'add' // also you can use html code
13 | ],
14 | 'removeButtonOptions' => [
15 | 'label' => 'remove'
16 | ]
17 | ])
18 | ->label(false);
19 | ```
20 |
21 | ## How to add content after the buttons
22 |
23 | You can add html content after `add` and `remove` buttons via `extraButtons`.
24 |
25 | ```php
26 | echo $form->field($model, 'field')->widget(MultipleInput::className(), [
27 | 'rendererClass' => \unclead\multipleinput\renderers\ListRenderer::class,
28 | 'extraButtons' => function ($model, $index, $context) {
29 | return Html::tag('span', '', ['class' => "btn-show-hide-{$index} glyphicon glyphicon-eye-open btn btn-info"]);
30 | },
31 | ])
32 | ->label(false);
33 | ```
34 |
35 | ## Work with an empty list
36 |
37 | In some cases, you need to have the ability to delete all rows in the list. For this purpose, you can use option `allowEmptyList` like in the example below:
38 |
39 | ```php
40 | echo $form->field($model, 'emails')->widget(MultipleInput::className(), [
41 | 'max' => 5,
42 | 'allowEmptyList' => true
43 | ])
44 | ->label(false);
45 | ```
46 |
47 | Also, you can set `0` in `min` option if you don't need the first blank row when data is empty.
48 |
49 | ## Guess column title
50 |
51 | Sometimes you can use the widget without defining columns but you want to have the column header of the table. In this case, you can use the option `enableGuessTitle` like in the example below:
52 |
53 | ```php
54 | echo $form->field($model, 'emails')->widget(MultipleInput::className(), [
55 | 'max' => 5,
56 | 'allowEmptyList' => true,
57 | 'enableGuessTitle' => true
58 | ])
59 | ->label(false);
60 | ```
61 |
62 | ## Ajax loading of a widget
63 |
64 | Assume you want to load a widget via ajax and then show it inside the modal window. In this case, you MUST:
65 |
66 | * Ensure that you specified the ID of the widget otherwise the widget will get a random ID and it can be the same as the ID of others elements on the page.
67 | * Ensure that you use the widget inside ActiveForm because it works incorrectly in this case.
68 |
69 | ## Use of a widget's placeholder
70 |
71 | You can use a placeholder `{multiple_index}` in a widget configuration, e.g. for implementation of dependent drop-down lists.
72 |
73 | ```php
74 | = $form->field($model, 'field')->widget(MultipleInput::className(), [
75 | 'id' => 'my_id',
76 | 'allowEmptyList' => false,
77 | 'rowOptions' => [
78 | 'id' => 'row{multiple_index_my_id}',
79 | ],
80 | 'columns' => [
81 | [
82 | 'name' => 'category',
83 | 'type' => 'dropDownList',
84 | 'title' => 'Category',
85 | 'defaultValue' => '1',
86 | 'items' => [
87 | '1' => 'Test 1',
88 | '2' => 'Test 2',
89 | '3' => 'Test 3',
90 | '4' => 'Test 4',
91 | ],
92 | 'options' => [
93 | 'onchange' => <<< JS
94 | $.post("list?id=" + $(this).val(), function(data){
95 | console.log(data);
96 | $("select#subcat-{multiple_index_my_id}").html(data);
97 | });
98 | JS
99 | ],
100 | ],
101 | [
102 | 'name' => 'subcategory',
103 | 'type' => 'dropDownList',
104 | 'title' => 'Subcategory',
105 | 'items' => [],
106 | 'options'=> [
107 | 'id' => 'subcat-{multiple_index_my_id}'
108 | ],
109 | ],
110 | ]
111 | ]);
112 | ?>
113 | ```
114 |
115 | **Important** Ensure that you added ID of widget to a base placeholder `multiple_index`
116 |
117 | ## Custom index of the row
118 |
119 | Assume that you want to set a specific index for each row. In this case, you can pass the `data` attribute as an associative array as in the example below:
120 |
121 | ```php
122 | = $form->field($model, 'field')->widget(MultipleInput::className(), [
123 | 'allowEmptyList' => false,
124 | 'data' => [
125 | 3 => [
126 | 'day' => '27.02.2015',
127 | 'user_id' => 31,
128 | 'priority' => 1,
129 | 'enable' => 1
130 | ],
131 |
132 | 'some-key' => [
133 | 'day' => '27.02.2015',
134 | 'user_id' => 33,
135 | 'priority' => 2,
136 | 'enable' => 0
137 | ],
138 | ]
139 |
140 | ...
141 | ```
142 |
143 | ## Embedded MultipleInput widget
144 |
145 | You can use nested `MultipleInput` as in the example below:
146 |
147 | ```php
148 | echo MultipleInput::widget([
149 | 'model' => $model,
150 | 'attribute' => 'questions',
151 | 'attributeOptions' => $commonAttributeOptions,
152 | 'columns' => [
153 | [
154 | 'name' => 'question',
155 | 'type' => 'textarea',
156 | ],
157 | [
158 | 'name' => 'answers',
159 | 'type' => MultipleInput::class,
160 | 'options' => [
161 | 'attributeOptions' => $commonAttributeOptions,
162 | 'columns' => [
163 | [
164 | 'name' => 'right',
165 | 'type' => MultipleInputColumn::TYPE_CHECKBOX
166 | ],
167 | [
168 | 'name' => 'answer'
169 | ]
170 | ]
171 | ]
172 | ]
173 | ],
174 | ]);
175 | ```
176 |
177 | But in this case, you have to pass `attributeOptions` to the widget otherwise, you will not be able to use ajax or client-side validation of data.
178 |
179 | ## Client validation
180 |
181 | Apart from ajax validation, you can use client validation but in this case, you MUST set property `form`. Also, ensure that you set `enableClientValidation` to `true` value in property `attributeOptions`. If you want to use client validation for a particular column you can use the property `attributeOptions`. An example of using client validation is listed below:
182 |
183 | ```php
184 | = TabularInput::widget([
185 | 'models' => $models,
186 | 'form' => $form,
187 | 'attributeOptions' => [
188 | 'enableAjaxValidation' => true,
189 | 'enableClientValidation' => false,
190 | 'validateOnChange' => false,
191 | 'validateOnSubmit' => true,
192 | 'validateOnBlur' => false,
193 | ],
194 | 'columns' => [
195 | [
196 | 'name' => 'id',
197 | 'type' => TabularColumn::TYPE_HIDDEN_INPUT
198 | ],
199 | [
200 | 'name' => 'title',
201 | 'title' => 'Title',
202 | 'type' => TabularColumn::TYPE_TEXT_INPUT,
203 | 'attributeOptions' => [
204 | 'enableClientValidation' => true,
205 | 'validateOnChange' => true,
206 | ],
207 | 'enableError' => true
208 | ],
209 | [
210 | 'name' => 'description',
211 | 'title' => 'Description',
212 | ],
213 | ],
214 | ]) ?>
215 | ```
216 |
217 | In the example above we use client validation for column `title` and ajax validation for column `description`. As you can seee we also enabled `validateOnChange` for column `title` thus you can use all client-side options from the `ActiveField` class.
218 |
219 |
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | You can find the source code of examples [here](https://github.com/unclead/yii2-multiple-input/tree/master/examples)
4 |
5 | ## One column
6 |
7 | 
8 |
9 | For example, your application contains the model `User` that has the related model `UserEmail` You can add virtual attribute `emails` for collect emails from a form and then you can save them to the database.
10 |
11 | In this case, you can use `yii2-multiple-input` widget for supporting multiple inputs how to describe below.
12 |
13 | First of all, we have to declare a virtual attribute in the model
14 |
15 | ```php
16 | class ExampleModel extends Model
17 | {
18 | /**
19 | * @var array virtual attribute for keeping emails
20 | */
21 | public $emails;
22 | ```
23 |
24 | Then we have to use `MultipleInput` widget for rendering form field in the view file
25 |
26 | ```php
27 | use yii\bootstrap\ActiveForm;
28 | use unclead\multipleinput\MultipleInput;
29 | use unclead\multipleinput\examples\models\ExampleModel;
30 | use yii\helpers\Html;
31 |
32 | /* @var $this \yii\base\View */
33 | /* @var $model ExampleModel */
34 | ?>
35 |
36 | true,
38 | 'enableClientValidation' => false,
39 | 'validateOnChange' => false,
40 | 'validateOnSubmit' => true,
41 | 'validateOnBlur' => false,
42 | ]);?>
43 |
44 | = $form->field($model, 'emails')->widget(MultipleInput::className(), [
45 | 'max' => 4,
46 | ]);
47 | ?>
48 | = Html::submitButton('Update', ['class' => 'btn btn-success']);?>
49 |
50 | ```
51 |
52 | Options `max` means that a user is able to input only 4 emails
53 |
54 | For validation emails, you can use the following code
55 |
56 | ```php
57 | /**
58 | * Email validation.
59 | *
60 | * @param $attribute
61 | */
62 | public function validateEmails($attribute)
63 | {
64 | $items = $this->$attribute;
65 |
66 | if (!is_array($items)) {
67 | $items = [];
68 | }
69 |
70 | foreach ($items as $index => $item) {
71 | $validator = new EmailValidator();
72 | $error = null;
73 | $validator->validate($item, $error);
74 | if (!empty($error)) {
75 | $key = $attribute . '[' . $index . ']';
76 | $this->addError($key, $error);
77 | }
78 | }
79 | }
80 | ```
81 |
82 | ## Multiple columns
83 |
84 | 
85 |
86 | For example, you want to have an interface for manage a user schedule. For simplicity, we will store the schedule in json string.
87 |
88 | In this case, you can use `yii2-multiple-input` widget for supporting multiple inputs how to describe below.
89 |
90 | Our test model can look like as the following snippet
91 |
92 | ```php
93 | class ExampleModel extends Model
94 | {
95 | public $schedule;
96 |
97 | public function init()
98 | {
99 | parent::init();
100 |
101 | $this->schedule = [
102 | [
103 | 'day' => '27.02.2015',
104 | 'user_id' => 1,
105 | 'priority' => 1
106 | ],
107 | [
108 | 'day' => '27.02.2015',
109 | 'user_id' => 2,
110 | 'priority' => 2
111 | ],
112 | ];
113 | }
114 | ```
115 |
116 | Then we have to use `MultipleInput` widget for rendering form field in the view file
117 |
118 | ```php
119 | use yii\bootstrap\ActiveForm;
120 | use unclead\multipleinput\MultipleInput;
121 | use unclead\multipleinput\examples\models\ExampleModel;
122 | use yii\helpers\Html;
123 |
124 | /* @var $this \yii\base\View */
125 | /* @var $model ExampleModel */
126 | ?>
127 |
128 | true,
130 | 'enableClientValidation' => false,
131 | 'validateOnChange' => false,
132 | 'validateOnSubmit' => true,
133 | 'validateOnBlur' => false,
134 | ]);?>
135 |
136 | = $form->field($model, 'schedule')->widget(MultipleInput::className(), [
137 | 'max' => 4,
138 | 'columns' => [
139 | [
140 | 'name' => 'user_id',
141 | 'type' => 'dropDownList',
142 | 'title' => 'User',
143 | 'defaultValue' => 1,
144 | 'items' => [
145 | 1 => 'User 1',
146 | 2 => 'User 2'
147 | ]
148 | ],
149 | [
150 | 'name' => 'day',
151 | 'type' => \kartik\date\DatePicker::className(),
152 | 'title' => 'Day',
153 | 'value' => function($data) {
154 | return $data['day'];
155 | },
156 | 'items' => [
157 | '0' => 'Saturday',
158 | '1' => 'Monday'
159 | ],
160 | 'options' => [
161 | 'pluginOptions' => [
162 | 'format' => 'dd.mm.yyyy',
163 | 'todayHighlight' => true
164 | ]
165 | ]
166 | ],
167 | [
168 | 'name' => 'priority',
169 | 'title' => 'Priority',
170 | 'enableError' => true,
171 | 'options' => [
172 | 'class' => 'input-priority'
173 | ]
174 | ]
175 | ]
176 | ]);
177 | ?>
178 | = Html::submitButton('Update', ['class' => 'btn btn-success']);?>
179 |
180 | ```
181 |
182 | For validation of the schedule you can use the following code
183 |
184 | ```php
185 | public function validateSchedule($attribute)
186 | {
187 | $requiredValidator = new RequiredValidator();
188 |
189 | foreach($this->$attribute as $index => $row) {
190 | $error = null;
191 | $requiredValidator->validate($row['priority'], $error);
192 | if (!empty($error)) {
193 | $key = $attribute . '[' . $index . '][priority]';
194 | $this->addError($key, $error);
195 | }
196 | }
197 | }
198 | ```
199 |
200 | For example, you keep some data in json format in an attribute of a model. Imagine that it is an abstract user schedule with keys: user\_id, day, priority
201 |
202 | On the edit page, you want to be able to manage this schedule and you can you yii2-multiple-input widget like in the following code
203 |
204 | ```php
205 | use unclead\multipleinput\MultipleInput;
206 |
207 | ...
208 |
209 | = $form->field($model, 'schedule')->widget(MultipleInput::className(), [
210 | 'max' => 4,
211 | 'columns' => [
212 | [
213 | 'name' => 'user_id',
214 | 'type' => 'dropDownList',
215 | 'title' => 'User',
216 | 'defaultValue' => 1,
217 | 'items' => [
218 | 1 => 'User 1',
219 | 2 => 'User 2'
220 | ]
221 | ],
222 | [
223 | 'name' => 'day',
224 | 'type' => \kartik\date\DatePicker::className(),
225 | 'title' => 'Day',
226 | 'value' => function($data) {
227 | return $data['day'];
228 | },
229 | 'items' => [
230 | '0' => 'Saturday',
231 | '1' => 'Monday'
232 | ],
233 | 'options' => [
234 | 'pluginOptions' => [
235 | 'format' => 'dd.mm.yyyy',
236 | 'todayHighlight' => true
237 | ]
238 | ],
239 | 'headerOptions' => [
240 | 'style' => 'width: 250px;',
241 | 'class' => 'day-css-class'
242 | ]
243 | ],
244 | [
245 | 'name' => 'priority',
246 | 'enableError' => true,
247 | 'title' => 'Priority',
248 | 'options' => [
249 | 'class' => 'input-priority'
250 | ]
251 | ],
252 | [
253 | 'name' => 'comment',
254 | 'type' => 'static',
255 | 'value' => function($data) {
256 | return Html::tag('span', 'static content', ['class' => 'label label-info']);
257 | },
258 | 'headerOptions' => [
259 | 'style' => 'width: 70px;',
260 | ]
261 | ]
262 | ]
263 | ]);
264 | ?>
265 | ```
266 |
267 | ## Tabular input
268 |
269 | For example, you want to have an interface for manage some abstract items via tabular input.
270 |
271 | In this case, you can use `yii2-multiple-input` widget for supporting tabular input how to describe below.
272 |
273 | Our test model can look like as the following snippet
274 |
275 | ```php
276 | namespace unclead\multipleinput\examples\models;
277 |
278 | use Yii;
279 | use yii\base\Model;
280 | // you have to install https://github.com/vova07/yii2-fileapi-widget
281 | use vova07\fileapi\behaviors\UploadBehavior;
282 |
283 | /**
284 | * Class Item
285 | * @package unclead\multipleinput\examples\models
286 | */
287 | class Item extends Model
288 | {
289 | public $title;
290 | public $description;
291 | public $file;
292 | public $date;
293 |
294 | public function behaviors()
295 | {
296 | return [
297 | 'uploadBehavior' => [
298 | 'class' => UploadBehavior::className(),
299 | 'attributes' => [
300 | 'file' => [
301 | 'path' => Yii::getAlias('@webroot') . '/images/',
302 | 'tempPath' => Yii::getAlias('@webroot') . '/images/tmp/',
303 | 'url' => '/images/'
304 | ],
305 | ]
306 | ]
307 | ];
308 | }
309 |
310 | public function rules()
311 | {
312 | return [
313 | [['title', 'description'], 'required'],
314 | ['file', 'safe']
315 | ];
316 | }
317 | }
318 | ```
319 |
320 | Then we have to use `TabularInput` widget for rendering form field in the view file
321 |
322 | Since version **2.18.0** you can configure `columnOptions` also.
323 |
324 | ```php
325 |
335 |
336 | 'tabular-form',
338 | 'enableAjaxValidation' => true,
339 | 'enableClientValidation' => false,
340 | 'validateOnChange' => false,
341 | 'validateOnSubmit' => true,
342 | 'validateOnBlur' => false,
343 | 'options' => [
344 | 'enctype' => 'multipart/form-data'
345 | ]
346 | ]) ?>
347 |
348 | = TabularInput::widget([
349 | 'models' => $models,
350 | 'attributeOptions' => [
351 | 'enableAjaxValidation' => true,
352 | 'enableClientValidation' => false,
353 | 'validateOnChange' => false,
354 | 'validateOnSubmit' => true,
355 | 'validateOnBlur' => false,
356 | ],
357 | 'columns' => [
358 | [
359 | 'name' => 'title',
360 | 'title' => 'Title',
361 | 'type' => \unclead\multipleinput\MultipleInputColumn::TYPE_TEXT_INPUT,
362 | ],
363 | [
364 | 'name' => 'description',
365 | 'title' => 'Description',
366 | ],
367 | [
368 | 'name' => 'file',
369 | 'title' => 'File',
370 | 'type' => \vova07\fileapi\Widget::className(),
371 | 'options' => [
372 | 'settings' => [
373 | 'url' => ['site/fileapi-upload']
374 | ]
375 | ],
376 | 'columnOptions' => [
377 | 'style' => 'width: 250px;',
378 | 'class' => 'custom-css-class'
379 | ]
380 | ],
381 | [
382 | 'name' => 'date',
383 | 'type' => \kartik\date\DatePicker::className(),
384 | 'title' => 'Day',
385 | 'options' => [
386 | 'pluginOptions' => [
387 | 'format' => 'dd.mm.yyyy',
388 | 'todayHighlight' => true
389 | ]
390 | ],
391 | 'headerOptions' => [
392 | 'style' => 'width: 250px;',
393 | 'class' => 'day-css-class'
394 | ]
395 | ],
396 | ],
397 | ]) ?>
398 |
399 |
400 | = Html::submitButton('Update', ['class' => 'btn btn-success']);?>
401 |
402 | ```
403 |
404 | Your action can look like the following code
405 |
406 | ```php
407 | /**
408 | * Class TabularInputAction
409 | * @package unclead\multipleinput\examples\actions
410 | */
411 | class TabularInputAction extends Action
412 | {
413 | public function run()
414 | {
415 | Yii::setAlias('@unclead-examples', realpath(__DIR__ . '/../'));
416 |
417 | $models = [new Item()];
418 | $request = Yii::$app->getRequest();
419 | if ($request->isPost && $request->post('ajax') !== null) {
420 | $data = Yii::$app->request->post('Item', []);
421 | foreach (array_keys($data) as $index) {
422 | $models[$index] = new Item();
423 | }
424 | Model::loadMultiple($models, Yii::$app->request->post());
425 | Yii::$app->response->format = Response::FORMAT_JSON;
426 | $result = ActiveForm::validateMultiple($models);
427 | return $result;
428 | }
429 |
430 | if (Model::loadMultiple($models, Yii::$app->request->post())) {
431 | // your magic
432 | }
433 |
434 |
435 | return $this->controller->render('@unclead-examples/views/tabular-input.php', ['models' => $models]);
436 | }
437 | }
438 | ```
439 |
440 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Yii2 multiple input
2 | site_description: 'Yii2 widget to handle multiple inputs for an attribute of a model'
3 | site_author: 'Eugene Tupikov'
4 | docs_dir: docs/
5 | repo_name: 'unclead/yii2-multiple-input'
6 | repo_url: 'https://github.com/unclead/yii2-multiple-input'
7 | nav:
8 | - About: index.md
9 | - Installation: installation.md
10 | - Getting started: getting-started.md
11 | - Configuration: configuration.md
12 | - Renderers: renderers.md
13 | - JS events: javascript-events.md
14 | - Clonning: clonning.md
15 | - Tips and Tricks: tips-and-tricks.md
16 | - Custom icons: icons.md
17 | - Usage: usage.md
18 | theme:
19 | name: 'material'
20 |
--------------------------------------------------------------------------------
/src/MultipleInput.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | class MultipleInput extends InputWidget
29 | {
30 | const POS_HEADER = RendererInterface::POS_HEADER;
31 | const POS_ROW = RendererInterface::POS_ROW;
32 | const POS_ROW_BEGIN = RendererInterface::POS_ROW_BEGIN;
33 | const POS_FOOTER = RendererInterface::POS_FOOTER;
34 |
35 | const THEME_DEFAULT = 'default';
36 | const THEME_BS = 'bootstrap';
37 |
38 | const ICONS_SOURCE_GLYPHICONS = 'glyphicons';
39 | const ICONS_SOURCE_FONTAWESOME = 'fa';
40 |
41 | /**
42 | * @var ActiveRecordInterface[]|array[] input data
43 | */
44 | public $data;
45 |
46 | /**
47 | * @var array columns configuration
48 | */
49 | public $columns = [];
50 |
51 | /**
52 | * @var integer maximum number of rows
53 | */
54 | public $max;
55 |
56 | /**
57 | * @var array client-side attribute options, e.g. enableAjaxValidation. You may use this property in case when
58 | * you use widget without a model, since in this case widget is not able to detect client-side options
59 | * automatically.
60 | */
61 | public $attributeOptions = [];
62 |
63 | /**
64 | * @var array the HTML options for the `remove` button
65 | */
66 | public $removeButtonOptions;
67 |
68 | /**
69 | * @var array the HTML options for the `add` button
70 | */
71 | public $addButtonOptions;
72 |
73 | /**
74 | * @var array the HTML options for the `clone` button
75 | */
76 | public $cloneButtonOptions;
77 |
78 | /**
79 | * @var bool whether to allow the empty list
80 | */
81 | public $allowEmptyList = false;
82 |
83 | /**
84 | * @var bool whether to guess column title in case if there is no definition of columns
85 | */
86 | public $enableGuessTitle = false;
87 |
88 | /**
89 | * @var int minimum number of rows
90 | */
91 | public $min;
92 |
93 | /**
94 | * @var string|array position of add button.
95 | */
96 | public $addButtonPosition;
97 |
98 | /**
99 | * @var array|\Closure the HTML attributes for the table body rows. This can be either an array
100 | * specifying the common HTML attributes for all body rows, or an anonymous function that
101 | * returns an array of the HTML attributes. It should have the following signature:
102 | *
103 | * ```php
104 | * function ($model, $index, $context)
105 | * ```
106 | *
107 | * - `$model`: the current data model being rendered
108 | * - `$index`: the zero-based index of the data model in the model array
109 | * - `$context`: the MultipleInput widget object
110 | *
111 | */
112 | public $rowOptions = [];
113 |
114 | /**
115 | * @var string the name of column class. You can specify your own class to extend base functionality.
116 | * Defaults to `unclead\multipleinput\MultipleInputColumn`
117 | */
118 | public $columnClass;
119 |
120 | /**
121 | * @var string the name of renderer class. Defaults to `unclead\multipleinput\renderers\TableRenderer`.
122 | * @since 1.4
123 | */
124 | public $rendererClass;
125 |
126 | /**
127 | * @var bool whether the widget is embedded or not.
128 | * @internal this property is used for internal purposes. Do not use it in your code.
129 | */
130 | public $isEmbedded = false;
131 |
132 | /**
133 | * @var ActiveForm an instance of ActiveForm which you have to pass in case of using client validation
134 | * @since 2.1
135 | */
136 | public $form;
137 |
138 | /**
139 | * @var bool allow sorting.
140 | * @internal this property is used when need to allow sorting rows.
141 | */
142 | public $sortable = false;
143 |
144 | /**
145 | * @var bool whether to render inline error for all input. Default to `false`. Can be override in `columns`
146 | * @since 2.10
147 | */
148 | public $enableError = false;
149 |
150 | /**
151 | * @var bool whether to render clone button. Default to `false`.
152 | */
153 | public $cloneButton = false;
154 |
155 | /**
156 | * @var string|\Closure the HTML content that will be rendered after the buttons.
157 | *
158 | * ```php
159 | * function ($model, $index, $context)
160 | * ```
161 | *
162 | * - `$model`: the current data model being rendered
163 | * - `$index`: the zero-based index of the data model in the model array
164 | * - `$context`: the MultipleInput widget object
165 | *
166 | */
167 | public $extraButtons;
168 |
169 | /**
170 | * @var array CSS grid classes for horizontal layout. This must be an array with these keys:
171 | * - 'offsetClass' the offset grid class to append to the wrapper if no label is rendered
172 | * - 'labelClass' the label grid class
173 | * - 'wrapperClass' the wrapper grid class
174 | * - 'errorClass' the error grid class
175 | */
176 | public $layoutConfig = [];
177 |
178 | /**
179 | * @var array
180 | * --icon library classes mapped for various controls
181 | */
182 | public $iconMap = [
183 | self::ICONS_SOURCE_GLYPHICONS => [
184 | 'drag-handle' => 'glyphicon glyphicon-menu-hamburger',
185 | 'remove' => 'glyphicon glyphicon-remove',
186 | 'add' => 'glyphicon glyphicon-plus',
187 | 'clone' => 'glyphicon glyphicon-duplicate',
188 | ],
189 | self::ICONS_SOURCE_FONTAWESOME => [
190 | 'drag-handle' => 'fa fa-bars',
191 | 'remove' => 'fa fa-times',
192 | 'add' => 'fa fa-plus',
193 | 'clone' => 'fa fa-files-o',
194 | ],
195 | ];
196 | /**
197 | * @var string the name of default icon library
198 | */
199 | public $iconSource = self::ICONS_SOURCE_GLYPHICONS;
200 |
201 | /**
202 | * @var string the CSS theme of the widget
203 | *
204 | * @todo Use bootstrap theme for BC. We can switch to default theme in major release
205 | */
206 | public $theme = self::THEME_BS;
207 |
208 | /**
209 | * @var bool
210 | */
211 | public $showGeneralError = false;
212 |
213 | /**
214 | * @var bool add a new line to the beginning of the list, not to the end
215 | */
216 | public $prepend = false;
217 |
218 | /**
219 | * Initialization.
220 | *
221 | * @throws \yii\base\InvalidConfigException
222 | */
223 | public function init()
224 | {
225 | if ($this->form !== null && !$this->form instanceof ActiveForm) {
226 | throw new InvalidConfigException('Property "form" must be an instance of yii\widgets\ActiveForm');
227 | }
228 |
229 | if ($this->showGeneralError && $this->field === null) {
230 | $this->showGeneralError = false;
231 | }
232 |
233 | $this->guessColumns();
234 | $this->initData();
235 |
236 | parent::init();
237 | }
238 |
239 | /**
240 | * Initializes data.
241 | */
242 | protected function initData()
243 | {
244 | if ($this->data !== null) {
245 | return;
246 | }
247 |
248 | if ($this->value !== null) {
249 | $this->data = $this->value;
250 | return;
251 | }
252 |
253 | if ($this->model instanceof Model) {
254 | $data = ($this->model->hasProperty($this->attribute) || isset($this->model->{$this->attribute}))
255 | ? ArrayHelper::getValue($this->model, $this->attribute, [])
256 | : [];
257 |
258 | if (!is_array($data) && empty($data)) {
259 | return;
260 | }
261 |
262 | if (!($data instanceof \Traversable)) {
263 | $data = (array) $data;
264 | }
265 |
266 | foreach ($data as $index => $value) {
267 | $this->data[$index] = $value;
268 | }
269 | }
270 | }
271 |
272 | /**
273 | * This function tries to guess the columns to show from the given data
274 | * if [[columns]] are not explicitly specified.
275 | */
276 | protected function guessColumns()
277 | {
278 | if (empty($this->columns)) {
279 | $column = [
280 | 'name' => $this->hasModel() ? $this->attribute : $this->name,
281 | 'type' => MultipleInputColumn::TYPE_TEXT_INPUT
282 | ];
283 |
284 | if ($this->enableGuessTitle && $this->hasModel()) {
285 | $column['title'] = $this->model->getAttributeLabel($this->attribute);
286 | }
287 | $this->columns[] = $column;
288 | }
289 | }
290 |
291 | /**
292 | * Run widget.
293 | */
294 | public function run()
295 | {
296 | $content = '';
297 | if ($this->isEmbedded === false && $this->hasModel()) {
298 | $content .= Html::hiddenInput(Html::getInputName($this->model, $this->attribute), null);
299 | }
300 |
301 | $content .= $this->createRenderer()->render();
302 |
303 | return $content;
304 | }
305 |
306 | /**
307 | * @return TableRenderer
308 | */
309 | protected function createRenderer()
310 | {
311 | if($this->sortable) {
312 | $drag = [
313 | 'name' => 'drag',
314 | 'type' => MultipleInputColumn::TYPE_DRAGCOLUMN,
315 | 'headerOptions' => [
316 | 'style' => 'width: 20px;',
317 | ]
318 | ];
319 |
320 | array_unshift($this->columns, $drag);
321 | }
322 |
323 | $available_themes = [
324 | self::THEME_BS,
325 | self::THEME_DEFAULT
326 | ];
327 |
328 | if (!in_array($this->theme, $available_themes, true)) {
329 | $this->theme = self::THEME_BS;
330 | }
331 |
332 | /**
333 | * set default icon map
334 | */
335 | $iconMap = array_key_exists($this->iconSource, $this->iconMap)
336 | ? $this->iconMap[$this->iconSource]
337 | : $this->iconMap[self::ICONS_SOURCE_GLYPHICONS];
338 |
339 | $config = [
340 | 'id' => $this->getId(),
341 | 'columns' => $this->columns,
342 | 'min' => $this->min,
343 | 'max' => $this->max,
344 | 'attributeOptions' => $this->attributeOptions,
345 | 'data' => $this->data,
346 | 'columnClass' => $this->columnClass !== null ? $this->columnClass : MultipleInputColumn::className(),
347 | 'allowEmptyList' => $this->allowEmptyList,
348 | 'addButtonPosition' => $this->addButtonPosition,
349 | 'rowOptions' => $this->rowOptions,
350 | 'context' => $this,
351 | 'form' => $this->form,
352 | 'sortable' => $this->sortable,
353 | 'enableError' => $this->enableError,
354 | 'cloneButton' => $this->cloneButton,
355 | 'extraButtons' => $this->extraButtons,
356 | 'layoutConfig' => $this->layoutConfig,
357 | 'iconMap' => $iconMap,
358 | 'theme' => $this->theme,
359 | 'prepend' => $this->prepend
360 | ];
361 |
362 | if ($this->showGeneralError) {
363 | $config['jsExtraSettings'] = [
364 | 'showGeneralError' => true
365 | ];
366 | }
367 |
368 | if ($this->removeButtonOptions !== null) {
369 | $config['removeButtonOptions'] = $this->removeButtonOptions;
370 | }
371 |
372 | if ($this->addButtonOptions !== null) {
373 | $config['addButtonOptions'] = $this->addButtonOptions;
374 | }
375 |
376 | if ($this->cloneButtonOptions !== null) {
377 | $config['cloneButtonOptions'] = $this->cloneButtonOptions;
378 | }
379 |
380 | $config['class'] = $this->rendererClass ?: TableRenderer::className();
381 |
382 | return Yii::createObject($config);
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/MultipleInputColumn.php:
--------------------------------------------------------------------------------
1 | enableError && !$this->context->model instanceof Model) {
34 | throw new InvalidConfigException('Property "enableError" available only when model is defined.');
35 | }
36 | }
37 |
38 | /**
39 | * Returns element's name.
40 | *
41 | * @param int|null|string $index current row index
42 | * @param bool $withPrefix whether to add prefix.
43 | *
44 | * @return string
45 | */
46 | public function getElementName($index, $withPrefix = true)
47 | {
48 | if ($index === null) {
49 | $index = '{' . $this->renderer->getIndexPlaceholder() . '}';
50 | }
51 |
52 | $elementName = $this->isRendererHasOneColumn()
53 | ? '[' . $this->name . '][' . $index . ']'
54 | : '[' . $index . '][' . $this->name . ']';
55 |
56 | if (!$withPrefix) {
57 | return $elementName;
58 | }
59 |
60 | $prefix = $this->getInputNamePrefix();
61 | if ($this->context->isEmbedded && strpos($prefix, $this->context->name) === false) {
62 | $prefix = $this->context->name;
63 | }
64 |
65 | return $prefix . $elementName . (empty($this->nameSuffix) ? '' : ('_' . $this->nameSuffix));
66 | }
67 |
68 | /**
69 | * @return bool
70 | */
71 | private function isRendererHasOneColumn()
72 | {
73 | $columns = \array_filter($this->renderer->columns, function(self $column) {
74 | return $column->type !== self::TYPE_DRAGCOLUMN;
75 | });
76 |
77 | return count($columns) === 1;
78 | }
79 |
80 | /**
81 | * Return prefix for name of input.
82 | *
83 | * @return string
84 | */
85 | protected function getInputNamePrefix()
86 | {
87 | $model = $this->context->model;
88 | if ($model instanceof Model) {
89 | if (empty($this->renderer->columns) || ($this->isRendererHasOneColumn() && $this->hasModelAttribute($this->name))) {
90 | return $model->formName();
91 | }
92 |
93 | return Html::getInputName($this->context->model, $this->context->attribute);
94 | }
95 |
96 | return $this->context->name;
97 | }
98 |
99 | protected function hasModelAttribute($name)
100 | {
101 | $model = $this->context->model;
102 |
103 | if ($model->hasProperty($name)) {
104 | return true;
105 | }
106 |
107 | if ($model instanceof ActiveRecordInterface && $model->hasAttribute($name)) {
108 | return true;
109 | }
110 |
111 | if ($model instanceof DynamicModel && isset($model->{$name})) {
112 | return true;
113 | }
114 |
115 | return false;
116 | }
117 |
118 | /**
119 | * @param int|string|null $index
120 | * @return null|string
121 | */
122 | public function getFirstError($index)
123 | {
124 | if ($index === null) {
125 | return null;
126 | }
127 |
128 | if ($this->isRendererHasOneColumn()) {
129 | $attribute = $this->name . '[' . $index . ']';
130 | } else {
131 | $attribute = $this->context->attribute . $this->getElementName($index, false);
132 | }
133 |
134 | $model = $this->context->model;
135 | if ($model instanceof Model) {
136 | return $model->getFirstError($attribute);
137 | }
138 |
139 | return null;
140 | }
141 |
142 | /**
143 | * @inheritdoc
144 | */
145 | protected function renderWidget($type, $name, $value, $options)
146 | {
147 | // Extend options in case of rendering embedded MultipleInput
148 | // We have to pass to the widget an original model and an attribute to be able get a first error from model
149 | // for embedded widget.
150 | if ($type === MultipleInput::className()) {
151 | $model = $this->context->model;
152 |
153 | // in case of embedding level 2 and more
154 | if (preg_match('/^([\w\.]+)(\[.*)$/', $this->context->attribute, $matches)) {
155 | $search = sprintf('%s[%s]%s', $model->formName(), $matches[1], $matches[2]);
156 | } else {
157 | $search = sprintf('%s[%s]', $model->formName(), $this->context->attribute);
158 | }
159 |
160 | $replace = $this->context->attribute;
161 |
162 | $attribute = str_replace($search, $replace, $name);
163 |
164 | $options['model'] = $model;
165 | $options['attribute'] = $attribute;
166 |
167 | // Remember current name and mark the widget as embedded to prevent
168 | // generation of wrong prefix in case the column is associated with AR relation
169 | // @see https://github.com/unclead/yii2-multiple-input/issues/92
170 | $options['name'] = $name;
171 | $options['isEmbedded'] = true;
172 | }
173 |
174 | return parent::renderWidget($type, $name, $value, $options);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/TabularColumn.php:
--------------------------------------------------------------------------------
1 | renderer->getIndexPlaceholder() . '}';
33 | }
34 |
35 | $elementName = '[' . $index . '][' . $this->name . ']';
36 | $prefix = $withPrefix ? $this->getModel()->formName() : '';
37 |
38 | return $prefix . $elementName . (empty($this->nameSuffix) ? '' : ('_' . $this->nameSuffix));
39 | }
40 |
41 | /**
42 | * Returns first error of the current model.
43 | *
44 | * @param $index
45 | * @return string
46 | */
47 | public function getFirstError($index)
48 | {
49 | return $this->getModel()->getFirstError($this->name);
50 | }
51 |
52 | /**
53 | * Ensure that model is an instance of yii\base\Model.
54 | *
55 | * @param $model
56 | * @return bool
57 | */
58 | protected function ensureModel($model)
59 | {
60 | return $model instanceof Model;
61 | }
62 |
63 | /**
64 | * @inheritdoc
65 | */
66 | public function setModel($model)
67 | {
68 | if ($model === null) {
69 | $model = \Yii::createObject(['class' => $this->context->modelClass]);
70 | }
71 |
72 | parent::setModel($model);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/TabularInput.php:
--------------------------------------------------------------------------------
1 | [
177 | 'drag-handle' => 'glyphicon glyphicon-menu-hamburger',
178 | 'remove' => 'glyphicon glyphicon-remove',
179 | 'add' => 'glyphicon glyphicon-plus',
180 | 'clone' => 'glyphicon glyphicon-duplicate',
181 | ],
182 | self::ICONS_SOURCE_FONTAWESOME => [
183 | 'drag-handle' => 'fa fa-bars',
184 | 'remove' => 'fa fa-times',
185 | 'add' => 'fa fa-plus',
186 | 'clone' => 'fa fa-files-o',
187 | ],
188 | ];
189 |
190 | /**
191 | * @var string the CSS theme of the widget
192 | *
193 | * @todo Use bootstrap theme for BC. We can switch to default theme in major release
194 | */
195 | public $theme = self::THEME_BS;
196 |
197 | /**
198 | * @var string the name of default icon library
199 | */
200 | public $iconSource = self::ICONS_SOURCE_GLYPHICONS;
201 |
202 | /**
203 | * @var bool add a new line to the beginning of the list, not to the end
204 | */
205 | public $prepend = false;
206 |
207 | /**
208 | * Initialization.
209 | *
210 | * @throws \yii\base\InvalidConfigException
211 | */
212 | public function init()
213 | {
214 | if (empty($this->models) && !$this->modelClass) {
215 | throw new InvalidConfigException('You must at least specify "models" or "modelClass"');
216 | }
217 |
218 | if ($this->form !== null && !$this->form instanceof ActiveForm) {
219 | throw new InvalidConfigException('Property "form" must be an instance of yii\widgets\ActiveForm');
220 | }
221 |
222 | if (!is_array($this->models)) {
223 | throw new InvalidConfigException('Property "models" must be an array');
224 | }
225 |
226 | if ($this->models) {
227 | $modelClasses = [];
228 | foreach ($this->models as $model) {
229 | if (!$model instanceof Model) {
230 | throw new InvalidConfigException('Model has to be an instance of yii\base\Model');
231 | }
232 |
233 | $modelClasses[get_class($model)] = true;
234 | }
235 |
236 | if (count($modelClasses) > 1) {
237 | throw new InvalidConfigException("You cannot use models of different classes");
238 | }
239 |
240 | $this->modelClass = key($modelClasses);
241 | }
242 |
243 | parent::init();
244 | }
245 |
246 | /**
247 | * Run widget.
248 | */
249 | public function run()
250 | {
251 | return $this->createRenderer()->render();
252 | }
253 |
254 | /**
255 | * @return TableRenderer
256 | */
257 | protected function createRenderer()
258 | {
259 | if($this->sortable) {
260 | $drag = [
261 | 'name' => 'drag',
262 | 'type' => TabularColumn::TYPE_DRAGCOLUMN,
263 | 'headerOptions' => [
264 | 'style' => 'width: 20px;',
265 | ]
266 | ];
267 |
268 | array_unshift($this->columns, $drag);
269 | }
270 |
271 | $available_themes = [
272 | self::THEME_BS,
273 | self::THEME_DEFAULT
274 | ];
275 |
276 | if (!in_array($this->theme, $available_themes, true)) {
277 | $this->theme = self::THEME_BS;
278 | }
279 |
280 | /**
281 | * set default icon map
282 | */
283 | $iconMap = array_key_exists($this->iconSource, $this->iconMap)
284 | ? $this->iconMap[$this->iconSource]
285 | : $this->iconMap[self::ICONS_SOURCE_GLYPHICONS];
286 |
287 | $config = [
288 | 'id' => $this->getId(),
289 | 'columns' => $this->columns,
290 | 'min' => $this->min,
291 | 'max' => $this->max,
292 | 'attributeOptions' => $this->attributeOptions,
293 | 'data' => $this->models,
294 | 'columnClass' => $this->columnClass !== null ? $this->columnClass : TabularColumn::className(),
295 | 'allowEmptyList' => $this->allowEmptyList,
296 | 'rowOptions' => $this->rowOptions,
297 | 'addButtonPosition' => $this->addButtonPosition,
298 | 'context' => $this,
299 | 'form' => $this->form,
300 | 'sortable' => $this->sortable,
301 | 'enableError' => $this->enableError,
302 | 'cloneButton' => $this->cloneButton,
303 | 'extraButtons' => $this->extraButtons,
304 | 'layoutConfig' => $this->layoutConfig,
305 | 'iconMap' => $iconMap,
306 | 'theme' => $this->theme,
307 | 'prepend' => $this->prepend
308 | ];
309 |
310 | if ($this->removeButtonOptions !== null) {
311 | $config['removeButtonOptions'] = $this->removeButtonOptions;
312 | }
313 |
314 | if ($this->addButtonOptions !== null) {
315 | $config['addButtonOptions'] = $this->addButtonOptions;
316 | }
317 |
318 | if ($this->cloneButtonOptions !== null) {
319 | $config['cloneButtonOptions'] = $this->cloneButtonOptions;
320 | }
321 |
322 | if (!$this->rendererClass) {
323 | $this->rendererClass = TableRenderer::className();
324 | }
325 |
326 | $config['class'] = $this->rendererClass ?: TableRenderer::className();
327 |
328 | return Yii::createObject($config);
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/src/assets/FontAwesomeAsset.php:
--------------------------------------------------------------------------------
1 | 'text/css', 'integrity'=>'sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ', 'crossorigin'=>'anonymous', 'media'=>'all', 'id'=>'font-awesome', 'rel'=>'stylesheet'],
24 | ];
25 |
26 | }
--------------------------------------------------------------------------------
/src/assets/MultipleInputAsset.php:
--------------------------------------------------------------------------------
1 | __DIR__ . '/src/',
27 | 'js' => [
28 | YII_DEBUG ? 'js/jquery.multipleInput.js' : 'js/jquery.multipleInput.min.js'
29 | ],
30 | 'css' => [
31 | YII_DEBUG ? 'css/multiple-input.css' : 'css/multiple-input.min.css'
32 | ],
33 | ], $config);
34 |
35 | parent::__construct($config);
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/assets/MultipleInputSortableAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = __DIR__ . '/src/';
26 |
27 | $this->js = [
28 | YII_DEBUG ? 'js/sortable.js' : 'js/sortable.min.js'
29 | ];
30 |
31 | $this->css = [
32 | YII_DEBUG ? 'css/sorting.css' : 'css/sorting.min.css'
33 | ];
34 |
35 | parent::init();
36 | }
37 | }
--------------------------------------------------------------------------------
/src/assets/src/css/multiple-input.css:
--------------------------------------------------------------------------------
1 | .multiple-input-list {
2 | }
3 |
4 | .multiple-input-list__item {
5 | margin-bottom: 5px;
6 | }
7 |
8 | .multiple-input-list__input {
9 | display: inline-block;
10 | width: 80%;
11 | }
12 |
13 | .multiple-input-list.no-buttons .multiple-input-list__input {
14 | display: block;
15 | width: 100%;
16 | }
17 |
18 | table.multiple-input-list {
19 | margin: 0;
20 | }
21 |
22 | table.multiple-input-list.table-renderer tbody tr > td {
23 | border: 0 !important;
24 | }
25 |
26 | table.multiple-input-list.table-renderer tr > td:first-child {
27 | padding-left: 0;
28 | }
29 |
30 | table.multiple-input-list.table-renderer tr > td:last-child {
31 | padding-right: 0;
32 | }
33 |
34 | table.multiple-input-list tr > th {
35 | border-bottom: 1px solid #dddddd;
36 | }
37 |
38 | .multiple-input-list.table-renderer .form-group {
39 | margin: 0 !important;
40 | }
41 |
42 | .multiple-input-list.list-renderer .form-group {
43 | margin-bottom: 10px !important;
44 | }
45 |
46 | .multiple-input-list.table-renderer .multiple-input-list__item .label {
47 | display: block;
48 | font-size: 13px;
49 | }
50 |
51 | .multiple-input-list.table-renderer .list-cell__button {
52 | width: 40px;
53 | }
54 |
55 | .multiple-input-list.list-renderer .list-cell__button {
56 | width: 40px;
57 | text-align: right;
58 | padding-right: 0;
59 | padding-left: 5px;
60 | }
61 |
62 | .multiple-input-list.list-renderer .list-cell__button:last-child {
63 | padding-right: 15px;
64 | }
65 |
66 | .multiple-input-list.list-renderer tbody .list-cell__button .btn{
67 | margin-top: 25px;
68 | }
69 |
70 | .multiple-input-list__item .radio,
71 | .multiple-input-list__item .checkbox {
72 | margin: 7px 0 7px 0;
73 | }
74 |
75 | .multiple-input-list__item .radio-list .radio,
76 | .multiple-input-list__item .checkbox-list .checkbox {
77 | margin: 0;
78 | }
79 |
80 | .multiple-input-list .multiple-input-list {
81 | margin-top: -5px;
82 | }
83 |
84 | @media (min-width: 768px) {
85 | .multiple-input-list.list-renderer tbody .list-cell__button .btn {
86 | margin-top: 0;
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/src/assets/src/css/multiple-input.min.css:
--------------------------------------------------------------------------------
1 | .multiple-input-list__item{margin-bottom:5px}.multiple-input-list__input{display:inline-block;width:80%}.multiple-input-list.no-buttons .multiple-input-list__input{display:block;width:100%}table.multiple-input-list{margin:0}table.multiple-input-list.table-renderer tbody tr>td{border:0!important}table.multiple-input-list.table-renderer tr>td:first-child{padding-left:0}table.multiple-input-list.table-renderer tr>td:last-child{padding-right:0}table.multiple-input-list tr>th{border-bottom:1px solid #ddd}.multiple-input-list.table-renderer .form-group{margin:0!important}.multiple-input-list.list-renderer .form-group{margin-bottom:10px!important}.multiple-input-list.table-renderer .multiple-input-list__item .label{display:block;font-size:13px}.multiple-input-list.table-renderer .list-cell__button{width:40px}.multiple-input-list.list-renderer .list-cell__button{width:70px;text-align:right;padding-right:15px}.multiple-input-list__item .checkbox,.multiple-input-list__item .radio{margin:7px 0}.multiple-input-list__item .checkbox-list .checkbox,.multiple-input-list__item .radio-list .radio{margin:0}.multiple-input-list .multiple-input-list{margin-top:-5px}
--------------------------------------------------------------------------------
/src/assets/src/css/sorting.css:
--------------------------------------------------------------------------------
1 | .drag-handle,
2 | .dragging,
3 | .dragging * {
4 | cursor: move !important;
5 | }
6 |
7 | .dragged {
8 | position: absolute;
9 | top: 0;
10 | opacity: .5;
11 | z-index: 2000;
12 | }
13 |
14 | .drag-handle {
15 | opacity: .5;
16 | padding: 7.5px;
17 | }
18 |
19 | .drag-handle:hover {
20 | opacity: 1;
21 | }
22 |
23 | tr.placeholder {
24 | display: block;
25 | background: red;
26 | position: relative;
27 | margin: 0;
28 | padding: 0;
29 | border: none;
30 | }
31 |
32 | tr.placeholder:before {
33 | content: "";
34 | position: absolute;
35 | width: 0;
36 | height: 0;
37 | border: 5px solid transparent;
38 | border-left-color: red;
39 | margin-top: -5px;
40 | left: -5px;
41 | border-right: none;
42 | }
43 |
44 | table.multiple-input-list.table-renderer tr > td.list-cell__drag {
45 | width: 35px;
46 | }
--------------------------------------------------------------------------------
/src/assets/src/css/sorting.min.css:
--------------------------------------------------------------------------------
1 | .drag-handle,.dragging,.dragging *{cursor:move!important}.dragged{position:absolute;top:0;opacity:.5;z-index:2000}.drag-handle{opacity:.5;padding:7.5px}.drag-handle:hover{opacity:1}tr.placeholder{display:block;background:red;position:relative;margin:0;padding:0;border:none}tr.placeholder:before{content:"";position:absolute;width:0;height:0;border:5px solid transparent;border-left-color:red;margin-top:-5px;left:-5px;border-right:none}table.multiple-input-list.table-renderer tr>td.list-cell__drag{width:35px}
--------------------------------------------------------------------------------
/src/assets/src/js/jquery.multipleInput.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | 'use strict';
3 |
4 | $.fn.multipleInput = function (method) {
5 | if (methods[method]) {
6 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
7 | } else if (typeof method === 'object' || !method) {
8 | return methods.init.apply(this, arguments);
9 | } else {
10 | $.error('Method ' + method + ' does not exist on jQuery.multipleInput');
11 | return false;
12 | }
13 | };
14 |
15 | var events = {
16 | /**
17 | * afterAddRow event is triggered after widget's initialization.
18 | * The signature of the event handler should be:
19 | * function (event)
20 | * where event is an Event object.
21 | *
22 | */
23 | afterInit: 'afterInit',
24 | /**
25 | * afterAddRow event is triggered after successful adding new row.
26 | * The signature of the event handler should be:
27 | * function (event, row)
28 | * where event is an Event object.
29 | *
30 | */
31 | beforeAddRow: 'beforeAddRow',
32 | /**
33 | * afterAddRow event is triggered after successful adding new row.
34 | * The signature of the event handler should be:
35 | * function (event, row)
36 | * where event is an Event object.
37 | *
38 | */
39 | afterAddRow: 'afterAddRow',
40 | /**
41 | * beforeDeleteRow event is triggered before row will be removed.
42 | * The signature of the event handler should be:
43 | * function (event, row)
44 | * where event is an Event object and row is html container of row for removal
45 | *
46 | * If the handler returns a boolean false, it will stop removal the row.
47 | */
48 | beforeDeleteRow: 'beforeDeleteRow',
49 |
50 | /**
51 | * afterAddRow event is triggered after successful removal the row.
52 | * The signature of the event handler should be:
53 | * function (event)
54 | * where event is an Event object.
55 | *
56 | */
57 | afterDeleteRow: 'afterDeleteRow',
58 |
59 | /**
60 | * afterDropRow event is triggered after drop the row in sortable mode.
61 | * The signature of the event handler should be:
62 | * function (event, row)
63 | * where event is an Event object and row is html container of dragged row
64 | */
65 | afterDropRow: 'afterDropRow'
66 | };
67 |
68 | var defaultOptions = {
69 | /**
70 | * the ID of widget
71 | */
72 | id: null,
73 |
74 | /**
75 | * the ID of related input in case of using widget for an active field
76 | */
77 | inputId: null,
78 |
79 | /**
80 | * the template of row
81 | */
82 | template: null,
83 |
84 | /**
85 | * array that collect js templates of widgets which uses in the columns
86 | */
87 | jsTemplates: [],
88 |
89 | /**
90 | * array of scripts which need to execute before initialization
91 | */
92 | jsInit: [],
93 |
94 | /**
95 | * how many row are allowed to render
96 | */
97 | max: 1,
98 |
99 | /**
100 | * a minimum number of rows
101 | */
102 | min: 1,
103 |
104 | /**
105 | * active form options of attributes
106 | */
107 | attributes: {},
108 |
109 | /**
110 | * default prefix of a widget's placeholder
111 | */
112 | indexPlaceholder: 'multiple_index',
113 |
114 | /**
115 | * whether need to show general error message or no
116 | */
117 | showGeneralError: false,
118 |
119 | /**
120 | * if need to prepend new row, not append
121 | */
122 | prepend: false
123 | };
124 |
125 | var isActiveFormEnabled = false;
126 |
127 | var methods = {
128 | init: function (options) {
129 | if (typeof options !== 'object') {
130 | console.error('Options must be an object');
131 | return;
132 | }
133 |
134 | var settings = $.extend(true, {}, defaultOptions, options || {}),
135 | $wrapper = $('#' + settings.id),
136 | form = $wrapper.closest('form'),
137 | inputId = settings.inputId;
138 |
139 | for (i in settings.jsInit) {
140 | window.eval(settings.jsInit[i]);
141 | }
142 |
143 | $wrapper.data('multipleInput', {
144 | settings: settings,
145 | currentIndex: 0
146 | });
147 |
148 | $wrapper.on('click.multipleInput', '.js-input-remove', function (e) {
149 | e.stopPropagation();
150 | removeInput($(this));
151 | });
152 |
153 | $wrapper.on('click.multipleInput', '.js-input-plus', function (e) {
154 | e.stopPropagation();
155 | addInput($(this));
156 | });
157 |
158 | $wrapper.on('click.multipleInput', '.js-input-clone', function (e) {
159 | e.stopPropagation();
160 | cloneInput($(this));
161 | });
162 |
163 | var i = 0,
164 | event = $.Event(events.afterInit);
165 |
166 | var intervalID = setInterval(function () {
167 | if (typeof form.data('yiiActiveForm') === 'object') {
168 | var attribute = form.yiiActiveForm('find', inputId),
169 | defaultAttributeOptions = {
170 | enableAjaxValidation: false,
171 | validateOnBlur: false,
172 | validateOnChange: false,
173 | validateOnType: false,
174 | validationDelay: 500
175 | };
176 |
177 | // fetch default attribute options from active from attribute
178 | if (typeof attribute === 'object') {
179 | $.each(attribute, function (key, value) {
180 | if (['id', 'input', 'container'].indexOf(key) === -1) {
181 | defaultAttributeOptions[key] = value;
182 | }
183 | });
184 |
185 | if (!settings.showGeneralError) {
186 | form.yiiActiveForm('remove', inputId);
187 | }
188 | }
189 |
190 | // append default options to option from settings
191 | $.each(settings.attributes, function (attribute, attributeOptions) {
192 | attributeOptions = $.extend({}, defaultAttributeOptions, attributeOptions);
193 | settings.attributes[attribute] = attributeOptions;
194 | });
195 |
196 | $wrapper.data('multipleInput').settings = settings;
197 |
198 | $wrapper.find('.multiple-input-list').find('input, select, textarea').each(function () {
199 | addActiveFormAttribute($(this));
200 | });
201 |
202 | $wrapper.data('multipleInput').currentIndex = findMaxRowIndex($wrapper);
203 | isActiveFormEnabled = true;
204 |
205 | clearInterval(intervalID);
206 | $wrapper.trigger(event);
207 | } else {
208 | i++;
209 | }
210 |
211 | // wait for initialization of ActiveForm a second
212 | // If after a second system could not detect ActiveForm it means
213 | // that widget is used without ActiveForm and we should just complete initialization of the widget
214 | if (form.length === 0 || i > 10) {
215 | clearInterval(intervalID);
216 | isActiveFormEnabled = false;
217 |
218 | if (typeof $wrapper.data('multipleInput') !== 'undefined') {
219 | $wrapper.data('multipleInput').currentIndex = findMaxRowIndex($wrapper);
220 | }
221 |
222 | $wrapper.trigger(event);
223 | }
224 | }, 100);
225 | },
226 |
227 | add: function (values) {
228 | addInput($(this), values);
229 | },
230 |
231 | remove: function (index) {
232 | var row = null;
233 | if (index !== undefined) {
234 | row = $(this).find('.js-input-remove:eq(' + index + ')');
235 | } else {
236 | row = $(this).find('.js-input-remove').last();
237 | }
238 |
239 | removeInput(row);
240 | },
241 |
242 | clear: function () {
243 | $(this).find('.js-input-remove').each(function () {
244 | removeInput($(this));
245 | });
246 | },
247 |
248 | option: function(name, value) {
249 | value = value || null;
250 |
251 | var data = $(this).data('multipleInput'),
252 | settings = data.settings;
253 | if (value === null) {
254 | if (!settings.hasOwnProperty(name)) {
255 | throw new Error('Option "' + name + '" does not exist');
256 | }
257 | return settings[name];
258 | } else if (settings.hasOwnProperty(name)) {
259 | settings[name] = value;
260 | data.settings = settings;
261 | $(this).data('multipleInput', data);
262 | }
263 | }
264 | };
265 |
266 | var cloneInput = function (btn) {
267 | let $wrapper = $(btn).closest('.multiple-input').first();
268 | let data = $wrapper.data('multipleInput');
269 | let settings = data.settings;
270 |
271 | let values = {};
272 |
273 | btn.closest('.multiple-input-list__item').find('input, select, textarea').each(function (k, v) {
274 | let $element = $(v);
275 |
276 | let id = getInputId($element);
277 | if (id) {
278 | // todo still doesn't work for sinlge column
279 | let columnName = id.replace(settings.inputId, '').replace(/-\d+-/, '');
280 | if ($element.is(':checkbox')) {
281 | if (!values.hasOwnProperty(columnName)) {
282 | values[columnName] = [];
283 | }
284 |
285 | if ($element.is(':checked')) {
286 | values[columnName].push($element.val());
287 | }
288 | } else {
289 | values[columnName] = $element.val();
290 | }
291 | }
292 | });
293 |
294 | addInput(btn, values);
295 | }
296 |
297 | var addInput = function (btn, rowValues) {
298 | rowValues = rowValues || {};
299 |
300 | let $wrapper = $(btn).closest('.multiple-input').first();
301 | let data = $wrapper.data('multipleInput');
302 | let settings = data.settings;
303 | let inputList = $wrapper.children('.multiple-input-list').first();
304 |
305 | if (settings.max !== null && getRowsCount($wrapper) >= settings.max) {
306 | return;
307 | }
308 |
309 | let newRowIndex = data.currentIndex + 1;
310 |
311 | let template = replaceAll('{' + settings.indexPlaceholder + '}', newRowIndex, settings.template);
312 | let $newRow = $(template);
313 |
314 | var beforeAddEvent = $.Event(events.beforeAddRow);
315 |
316 | $wrapper.trigger(beforeAddEvent, [$newRow, newRowIndex]);
317 | if (beforeAddEvent.result === false) {
318 | return;
319 | }
320 |
321 | $newRow.find('input, select, textarea').each(function (index, element) {
322 | let $element = $(element);
323 |
324 | let id = getInputId($element);
325 | if (id) {
326 | let columnName = id.replace(settings.inputId, '').replace(/-\d+-?/, '');
327 |
328 | if (rowValues.hasOwnProperty(columnName)) {
329 | let tag = $element.get(0).tagName;
330 |
331 | let inputValue = rowValues[columnName];
332 |
333 | switch (tag) {
334 | case 'INPUT':
335 | if ($element.is(':checkbox')) {
336 | if (inputValue.indexOf($element.val()) !== -1) {
337 | $element.prop('checked', true);
338 | }
339 | } else {
340 | $element.val(inputValue);
341 | }
342 |
343 | break;
344 |
345 | case 'TEXTAREA':
346 | $element.val(inputValue);
347 | break;
348 |
349 | case 'SELECT':
350 | if (inputValue && inputValue.indexOf('option') !== -1) {
351 | $element.append(inputValue);
352 | } else {
353 | var option = $element.find('option[value="' + inputValue + '"]');
354 | if (option.length) {
355 | $element.val(inputValue);
356 | }
357 | }
358 |
359 | break;
360 |
361 | default:
362 | break;
363 | }
364 | }
365 | }
366 | });
367 |
368 | if (settings.prepend) {
369 | $newRow.hide().prependTo(inputList).fadeIn(300);
370 | } else {
371 | $newRow.hide().appendTo(inputList).fadeIn(300);
372 | }
373 |
374 | let jsTemplate = null;
375 | for (var i in settings.jsTemplates) {
376 | jsTemplate = settings.jsTemplates[i];
377 | jsTemplate = replaceAll('{' + settings.indexPlaceholder + '}', newRowIndex, jsTemplate);
378 | jsTemplate = replaceAll('%7B' + settings.indexPlaceholder + '%7D', newRowIndex, jsTemplate);
379 |
380 | window.eval(jsTemplate);
381 | }
382 |
383 | // in order to initialize an active form attribute we need to find an input wrapper and we can do it
384 | // only after adding a new rows to dom tree
385 | if (isActiveFormEnabled) {
386 | $newRow.find('input, select, textarea').each(function (index, element) {
387 | let $element = $(element);
388 | addActiveFormAttribute($element);
389 | });
390 | }
391 |
392 | $wrapper.data('multipleInput').currentIndex = newRowIndex;
393 |
394 | var afterAddEvent = $.Event(events.afterAddRow);
395 | $wrapper.trigger(afterAddEvent, [$newRow, newRowIndex]);
396 | };
397 |
398 | var removeInput = function ($btn) {
399 | var $wrapper = $btn.closest('.multiple-input').first(),
400 | $toDelete = $btn.closest('.multiple-input-list__item'),
401 | data = $wrapper.data('multipleInput'),
402 | settings = data.settings;
403 |
404 | var rowsCount = getRowsCount($wrapper);
405 | if (rowsCount > settings.min) {
406 | var event = $.Event(events.beforeDeleteRow);
407 | $wrapper.trigger(event, [$toDelete, data.currentIndex]);
408 |
409 | if (event.result === false) {
410 | return;
411 | }
412 |
413 | if (isActiveFormEnabled) {
414 | $toDelete.find('input, select, textarea').each(function (index, ele) {
415 | removeActiveFormAttribute($(ele));
416 | });
417 | }
418 |
419 | $toDelete.fadeOut(300, function () {
420 | $(this).remove();
421 |
422 | event = $.Event(events.afterDeleteRow);
423 | $wrapper.trigger(event, [$toDelete, rowsCount]);
424 | });
425 | }
426 | };
427 |
428 | /**
429 | * Add an attribute to ActiveForm.
430 | *
431 | * @param input
432 | */
433 | var addActiveFormAttribute = function (input) {
434 | var id = getInputId(input);
435 |
436 | // skip if we could not get an ID of input
437 | if (id === null) {
438 | return;
439 | }
440 |
441 | var ele = $('#' + id),
442 | wrapper = ele.closest('.multiple-input').first(),
443 | form = ele.closest('form');
444 |
445 | // do not add attribute which are not the part of widget
446 | if (wrapper.length === 0) {
447 | return;
448 | }
449 |
450 | // check that input has been already added to the activeForm
451 | if (typeof form.yiiActiveForm('find', id) !== 'undefined') {
452 | return;
453 | }
454 |
455 | var data = wrapper.data('multipleInput'),
456 | attributeOptions = {};
457 |
458 | // try to find options for embedded attribute at first.
459 | // For example the id of new input is example-1-field-0.
460 | // We remove last index and check whether attribute with such id exists or not.
461 | var bareId = id.replace(/-\d+-([^\d]+)$/, '-$1');
462 | if (data.settings.attributes.hasOwnProperty(bareId)) {
463 | attributeOptions = data.settings.attributes[bareId];
464 | } else {
465 | // fallback in case of using flatten widget - just remove all digital indexes
466 | // and check whether attribute exists or not.
467 | bareId = replaceAll(/-\d+-/, '-', bareId);
468 | bareId = replaceAll(/-\d+/, '', bareId);
469 | if (data.settings.attributes.hasOwnProperty(bareId)) {
470 | attributeOptions = data.settings.attributes[bareId];
471 | }
472 | }
473 |
474 | form.yiiActiveForm('add', $.extend({}, attributeOptions, {
475 | 'id': id,
476 | 'input': '#' + id,
477 | 'container': '.field-' + id
478 | }));
479 | };
480 |
481 | /**
482 | * Removes an attribute from ActiveForm.
483 | */
484 | var removeActiveFormAttribute = function (ele) {
485 | var id = getInputId(ele);
486 |
487 | if (id === null) {
488 | return;
489 | }
490 |
491 | var form = $('#' + id).closest('form');
492 |
493 | if (form.length !== 0) {
494 | form.yiiActiveForm('remove', id);
495 | }
496 | };
497 |
498 | var getInputId = function ($input) {
499 | var id = $input.attr('id');
500 |
501 | if (typeof id === 'undefined') {
502 | id = $input.data('id');
503 | }
504 |
505 | if (typeof id === 'undefined') {
506 | return null;
507 | }
508 |
509 | return id;
510 | };
511 |
512 | var getRowsCount = function($wrapper) {
513 | return findRows($wrapper).length;
514 | };
515 |
516 | var findRows = function($wrapper) {
517 | return $wrapper
518 | .find('.multiple-input-list .multiple-input-list__item')
519 | .filter(function(){
520 | return $(this).parents('.multiple-input').first().attr('id') === $wrapper.attr('id');
521 | });
522 | }
523 |
524 | var findMaxRowIndex = function($wrapper) {
525 | let maxIndex = 0;
526 |
527 | findRows($wrapper).each(function(key, element) {
528 | let index = $(element).data('index');
529 | if (index > maxIndex) {
530 | maxIndex = index;
531 | }
532 | });
533 |
534 | return maxIndex;
535 | };
536 |
537 | var replaceAll = function (search, replace, subject) {
538 | if (!(subject instanceof String) && typeof subject !== 'string') {
539 | console.warn('Call replaceAll for non-string value: ' + subject);
540 | return subject;
541 | }
542 |
543 | return subject.split(search).join(replace);
544 | };
545 | })(window.jQuery);
546 |
--------------------------------------------------------------------------------
/src/assets/src/js/jquery.multipleInput.min.js:
--------------------------------------------------------------------------------
1 | !function(t){"use strict";t.fn.multipleInput=function(e){return o[e]?o[e].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof e&&e?(t.error("Method "+e+" does not exist on jQuery.multipleInput"),!1):o.init.apply(this,arguments)};var e="afterInit",i="beforeAddRow",n="afterAddRow",l="beforeDeleteRow",r="afterDeleteRow",a={id:null,inputId:null,template:null,jsTemplates:[],jsInit:[],max:1,min:1,attributes:{},indexPlaceholder:"multiple_index",showGeneralError:!1,prepend:!1},u=!1,o={init:function(i){if("object"==typeof i){var n=t.extend(!0,{},a,i||{}),l=t("#"+n.id),r=l.closest("form"),o=n.inputId;for(f in n.jsInit)window.eval(n.jsInit[f]);l.data("multipleInput",{settings:n,currentIndex:0}),l.on("click.multipleInput",".js-input-remove",(function(e){e.stopPropagation(),c(t(this))})),l.on("click.multipleInput",".js-input-plus",(function(e){e.stopPropagation(),p(t(this))})),l.on("click.multipleInput",".js-input-clone",(function(e){e.stopPropagation(),s(t(this))}));var f=0,m=t.Event(e),v=setInterval((function(){if("object"==typeof r.data("yiiActiveForm")){var e=r.yiiActiveForm("find",o),i={enableAjaxValidation:!1,validateOnBlur:!1,validateOnChange:!1,validateOnType:!1,validationDelay:500};"object"==typeof e&&(t.each(e,(function(t,e){-1===["id","input","container"].indexOf(t)&&(i[t]=e)})),n.showGeneralError||r.yiiActiveForm("remove",o)),t.each(n.attributes,(function(e,l){l=t.extend({},i,l),n.attributes[e]=l})),l.data("multipleInput").settings=n,l.find(".multiple-input-list").find("input, select, textarea").each((function(){d(t(this))})),l.data("multipleInput").currentIndex=g(l),u=!0,clearInterval(v),l.trigger(m)}else f++;(0===r.length||f>10)&&(clearInterval(v),u=!1,void 0!==l.data("multipleInput")&&(l.data("multipleInput").currentIndex=g(l)),l.trigger(m))}),100)}else console.error("Options must be an object")},add:function(e){p(t(this),e)},remove:function(e){var i=null;i=void 0!==e?t(this).find(".js-input-remove:eq("+e+")"):t(this).find(".js-input-remove").last(),c(i)},clear:function(){t(this).find(".js-input-remove").each((function(){c(t(this))}))},option:function(e,i){i=i||null;var n=t(this).data("multipleInput"),l=n.settings;if(null===i){if(!l.hasOwnProperty(e))throw new Error('Option "'+e+'" does not exist');return l[e]}l.hasOwnProperty(e)&&(l[e]=i,n.settings=l,t(this).data("multipleInput",n))}},s=function(e){let i=t(e).closest(".multiple-input").first().data("multipleInput").settings,n={};e.closest(".multiple-input-list__item").find("input, select, textarea").each((function(e,l){let r=t(l),a=m(r);if(a){let t=a.replace(i.inputId,"").replace(/-\d+-/,"");r.is(":checkbox")?(n.hasOwnProperty(t)||(n[t]=[]),r.is(":checked")&&n[t].push(r.val())):n[t]=r.val()}})),p(e,n)},p=function(e,l){l=l||{};let r=t(e).closest(".multiple-input").first(),a=r.data("multipleInput"),o=a.settings,s=r.children(".multiple-input-list").first();if(null!==o.max&&v(r)>=o.max)return;let p=a.currentIndex+1,c=I("{"+o.indexPlaceholder+"}",p,o.template),f=t(c);var h=t.Event(i);if(r.trigger(h,[f,p]),!1===h.result)return;f.find("input, select, textarea").each((function(e,i){let n=t(i),r=m(n);if(r){let t=r.replace(o.inputId,"").replace(/-\d+-?/,"");if(l.hasOwnProperty(t)){let e=n.get(0).tagName,i=l[t];switch(e){case"INPUT":n.is(":checkbox")?-1!==i.indexOf(n.val())&&n.prop("checked",!0):n.val(i);break;case"TEXTAREA":n.val(i);break;case"SELECT":if(i&&-1!==i.indexOf("option"))n.append(i);else n.find('option[value="'+i+'"]').length&&n.val(i)}}}})),o.prepend?f.hide().prependTo(s).fadeIn(300):f.hide().appendTo(s).fadeIn(300);let g=null;for(var x in o.jsTemplates)g=o.jsTemplates[x],g=I("{"+o.indexPlaceholder+"}",p,g),g=I("%7B"+o.indexPlaceholder+"%7D",p,g),window.eval(g);u&&f.find("input, select, textarea").each((function(e,i){let n=t(i);d(n)})),r.data("multipleInput").currentIndex=p;var y=t.Event(n);r.trigger(y,[f,p])},c=function(e){var i=e.closest(".multiple-input").first(),n=e.closest(".multiple-input-list__item"),a=i.data("multipleInput"),o=a.settings,s=v(i);if(s>o.min){var p=t.Event(l);if(i.trigger(p,[n,a.currentIndex]),!1===p.result)return;u&&n.find("input, select, textarea").each((function(e,i){f(t(i))})),n.fadeOut(300,(function(){t(this).remove(),p=t.Event(r),i.trigger(p,[n,s])}))}},d=function(e){var i=m(e);if(null!==i){var n=t("#"+i),l=n.closest(".multiple-input").first(),r=n.closest("form");if(0!==l.length&&void 0===r.yiiActiveForm("find",i)){var a=l.data("multipleInput"),u={},o=i.replace(/-\d+-([^\d]+)$/,"-$1");a.settings.attributes.hasOwnProperty(o)?u=a.settings.attributes[o]:(o=I(/-\d+-/,"-",o),o=I(/-\d+/,"",o),a.settings.attributes.hasOwnProperty(o)&&(u=a.settings.attributes[o])),r.yiiActiveForm("add",t.extend({},u,{id:i,input:"#"+i,container:".field-"+i}))}}},f=function(e){var i=m(e);if(null!==i){var n=t("#"+i).closest("form");0!==n.length&&n.yiiActiveForm("remove",i)}},m=function(t){var e=t.attr("id");return void 0===e&&(e=t.data("id")),void 0===e?null:e},v=function(t){return h(t).length},h=function(e){return e.find(".multiple-input-list .multiple-input-list__item").filter((function(){return t(this).parents(".multiple-input").first().attr("id")===e.attr("id")}))},g=function(e){let i=0;return h(e).each((function(e,n){let l=t(n).data("index");l>i&&(i=l)})),i},I=function(t,e,i){return i instanceof String||"string"==typeof i?i.split(t).join(e):(console.warn("Call replaceAll for non-string value: "+i),i)}}(window.jQuery);
--------------------------------------------------------------------------------
/src/components/BaseColumn.php:
--------------------------------------------------------------------------------
1 | [
76 | * ...
77 | * [
78 | * 'name' => 'column',
79 | * 'items' => function($data) {
80 | * // do your magic
81 | * }
82 | * ....
83 | * ]
84 | * ...
85 | *
86 | * ```
87 | */
88 | public $items;
89 |
90 | /**
91 | * @var array
92 | */
93 | public $options;
94 |
95 | /**
96 | * @var array the HTML attributes for the header cell tag.
97 | */
98 | public $headerOptions = [];
99 |
100 | /**
101 | * @var bool whether to render inline error for the input. Default to `false`
102 | */
103 | public $enableError = false;
104 |
105 | /**
106 | * @var array the default options for the error tag
107 | */
108 | public $errorOptions = ['class' => 'help-block help-block-error'];
109 |
110 | /**
111 | * @var BaseRenderer the renderer instance
112 | */
113 | public $renderer;
114 |
115 | /**
116 | * @var mixed the context of using a column. It is an instance of widget(MultipleInput or TabularInput).
117 | */
118 | public $context;
119 |
120 | /**
121 | * @var array client-side options of the attribute, e.g. enableAjaxValidation.
122 | * You can use this property for custom configuration of the column (attribute).
123 | * By default, the column will use options which are defined on widget level.
124 | *
125 | * @since 2.1
126 | */
127 | public $attributeOptions = [];
128 |
129 | /**
130 | * @var string the unique prefix for attribute's name to avoid id duplication e.g. in case of using Select2 widget.
131 | * @since 2.8
132 | */
133 | public $nameSuffix;
134 |
135 | /**
136 | * @var array|\Closure the HTML attributes for the indivdual table body column. This can be either an array
137 | * specifying the common HTML attributes for indivdual body column, or an anonymous function that
138 | * returns an array of the HTML attributes. It should have the following signature:
139 | *
140 | * ```php
141 | * function ($model, $index, $context)
142 | * ```
143 | *
144 | * - `$model`: the current data model being rendered
145 | * - `$index`: the zero-based index of the data model in the model array
146 | * - `$context`: the widget object
147 | *
148 | * @since 2.18.0
149 | */
150 | public $columnOptions = [];
151 |
152 | /**
153 | * @var string the template of input for customize view.
154 | * For example: '