├── .gitignore
├── src
├── interfaces
│ ├── TreeQueryInterface.php
│ └── TreeInterface.php
├── forms
│ └── MoveNodeForm.php
├── widgets
│ └── nestable
│ │ ├── NestableAsset.php
│ │ ├── assets
│ │ ├── jquery.nestable.css
│ │ └── jquery.nestable.js
│ │ └── Nestable.php
└── actions
│ ├── DeleteNodeAction.php
│ ├── CreateNodeAction.php
│ ├── UpdateNodeAction.php
│ ├── BaseAction.php
│ └── MoveNodeAction.php
├── LICENSE
├── composer.json
├── README.md
└── patch.diff
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 | composer.lock
--------------------------------------------------------------------------------
/src/interfaces/TreeQueryInterface.php:
--------------------------------------------------------------------------------
1 | findModel($id);
27 |
28 | return $model->deleteWithChildren();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/actions/CreateNodeAction.php:
--------------------------------------------------------------------------------
1 | modelClass);
24 |
25 | $params = Yii::$app->getRequest()->getBodyParams();
26 | $model->load($params);
27 |
28 | if (!$model->validate()) {
29 | return $model;
30 | }
31 |
32 | $roots = $model::find()->roots()->all();
33 |
34 | if (isset($roots[0])) {
35 | return $model->appendTo($roots[0])->save();
36 | } else {
37 | return $model->makeRoot()->save();
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Vitaly
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/actions/UpdateNodeAction.php:
--------------------------------------------------------------------------------
1 | findModel($id);
35 |
36 | $name = Yii::$app->request->post('name');
37 | $model->setAttribute($this->nameAttribute, $name);
38 |
39 | if (!$model->validate()) {
40 | return $model;
41 | }
42 |
43 | return $model->update(true, [$this->nameAttribute]);
44 | }
45 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voskobovich/yii2-tree-manager",
3 | "description": "Tree Manager using jquery.nestable plugin for Yii 2",
4 | "keywords": [
5 | "widget",
6 | "nested sets",
7 | "yii2",
8 | "nestable",
9 | "editor"
10 | ],
11 | "homepage": "https://github.com/voskobovich/yii2-tree-manager",
12 | "type": "yii2-widget",
13 | "license": "MIT",
14 | "support": {
15 | "issues": "https://github.com/voskobovich/yii2-tree-manager/issues",
16 | "source": "https://github.com/voskobovich/yii2-tree-manager"
17 | },
18 | "authors": [
19 | {
20 | "name": "Vitaly Voskobovich",
21 | "email": "vitaly@voskobovich.com",
22 | "homepage": "http://voskobovich.com"
23 | }
24 | ],
25 | "require": {
26 | "php": ">=5.4.0",
27 | "yiisoft/yii2": "^2.0.0",
28 | "yiisoft/yii2-bootstrap": "^2.0.0"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "voskobovich\\tree\\manager\\actions\\": "src/actions",
33 | "voskobovich\\tree\\manager\\behaviors\\": "src/behaviors",
34 | "voskobovich\\tree\\manager\\forms\\": "src/forms",
35 | "voskobovich\\tree\\manager\\interfaces\\": "src/interfaces",
36 | "voskobovich\\tree\\manager\\widgets\\nestable\\": "src/widgets/nestable"
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/actions/BaseAction.php:
--------------------------------------------------------------------------------
1 | modelClass) {
30 | throw new InvalidConfigException('Param "modelClass" must be contain model name with namespace.');
31 | }
32 | }
33 |
34 | /**
35 | * @param $id
36 | * @return ActiveRecord|TreeInterface
37 | * @throws NotFoundHttpException
38 | */
39 | public function findModel($id)
40 | {
41 | /** @var ActiveRecord $model */
42 | $model = $this->modelClass;
43 | /** @var ActiveRecord|TreeInterface $model */
44 | $model = $model::findOne($id);
45 |
46 | if ($model == null) {
47 | throw new NotFoundHttpException();
48 | }
49 |
50 | return $model;
51 | }
52 | }
--------------------------------------------------------------------------------
/src/actions/MoveNodeAction.php:
--------------------------------------------------------------------------------
1 | findModel($id);
29 |
30 | $form = new MoveNodeForm();
31 |
32 | $params = Yii::$app->getRequest()->getBodyParams();
33 | $form->load($params, '');
34 |
35 | if (!$form->validate()) {
36 | return $form;
37 | }
38 |
39 | try {
40 | if ($form->prev_id > 0) {
41 | $parentModel = $this->findModel($form->prev_id);
42 | if ($parentModel->isRoot()) {
43 | return $model->appendTo($parentModel)->save();
44 | } else {
45 | return $model->insertAfter($parentModel)->save();
46 | }
47 | } elseif ($form->next_id > 0) {
48 | $parentModel = $this->findModel($form->next_id);
49 | return $model->insertBefore($parentModel)->save();
50 | } elseif ($form->parent_id > 0) {
51 | $parentModel = $this->findModel($form->parent_id);
52 | return $model->appendTo($parentModel)->save();
53 | }
54 | } catch (Exception $ex) {
55 | }
56 |
57 | return false;
58 | }
59 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tree Manager for Yii2
2 |
3 | Виджет для управления деревом.
4 |
5 | Внимание!
6 | -----
7 | Виджет рассчитан на работу с поведениями Павла Зимакова:
8 |
9 | [Yii2 Adjacency List Behavior](https://github.com/paulzi/yii2-adjacency-list)
10 | [Yii2 Nested Sets Behavior](https://github.com/paulzi/yii2-nested-sets)
11 | [Yii2 Nested Intervals Behavior](https://github.com/paulzi/yii2-nested-intervals)
12 | [Yii2 Materialized Path Behavior](https://github.com/paulzi/yii2-materialized-path)
13 |
14 | Отличная статья на [Хабре](http://habrahabr.ru/post/266155/).
15 |
16 |
17 | Installation
18 | -------------
19 |
20 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
21 |
22 | Either run
23 |
24 | ```
25 | php composer.phar require --prefer-dist voskobovich/yii2-tree-manager "~1.0"
26 | ```
27 |
28 | or add
29 |
30 | ```
31 | "voskobovich/yii2-tree-manager": "~1.0"
32 | ```
33 |
34 | to the require section of your `composer.json` file.
35 |
36 |
37 | Usage
38 | -----
39 |
40 | 1. Подключите к вашей модели любое из указанных выше поведений
41 |
42 | 2. Подключите в контроллер дополнительные actions
43 |
44 | ```
45 | public function actions()
46 | {
47 | $modelClass = 'namespace\ModelName';
48 |
49 | return [
50 | 'moveNode' => [
51 | 'class' => 'voskobovich\tree\manager\actions\MoveNodeAction',
52 | 'modelClass' => $modelClass,
53 | ],
54 | 'deleteNode' => [
55 | 'class' => 'voskobovich\tree\manager\actions\DeleteNodeAction',
56 | 'modelClass' => $modelClass,
57 | ],
58 | 'updateNode' => [
59 | 'class' => 'voskobovich\tree\manager\actions\UpdateNodeAction',
60 | 'modelClass' => $modelClass,
61 | ],
62 | 'createNode' => [
63 | 'class' => 'voskobovich\tree\manager\actions\CreateNodeAction',
64 | 'modelClass' => $modelClass,
65 | ],
66 | ];
67 | }
68 | ```
69 |
70 | 3. Выведите виджет в удобном месте
71 |
72 | ```
73 | use \voskobovich\tree\manager\widgets\nestable\Nestable;
74 |
75 | = Nestable::widget([
76 | 'modelClass' => 'models\ModelName',
77 | ]) ?>
78 | ```
79 |
80 | Пример того, как выглядит виджет:
81 | -------------
82 |
83 | 
84 |
--------------------------------------------------------------------------------
/patch.diff:
--------------------------------------------------------------------------------
1 | From b51a022602ec7d36f12ab8b160f9f476995e13e7 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9?=
3 | <89125230101@mail.ru>
4 | Date: Tue, 27 Dec 2016 09:52:02 +0500
5 | Subject: [PATCH] fix Labels in modal box
6 |
7 | ---
8 | src/widgets/nestable/Nestable.php | 11 +++++++----
9 | 1 file changed, 7 insertions(+), 4 deletions(-)
10 |
11 | diff --git a/src/widgets/nestable/Nestable.php b/src/widgets/nestable/Nestable.php
12 | index 3df1c1f..24fa465 100644
13 | --- a/src/widgets/nestable/Nestable.php
14 | +++ b/src/widgets/nestable/Nestable.php
15 | @@ -119,7 +119,7 @@ class Nestable extends Widget
16 | }
17 |
18 | /** @var ActiveRecord|TreeInterface $model */
19 | - $model = $this->modelClass;
20 | + $model = new $this->modelClass;
21 |
22 | /** @var ActiveRecord[]|TreeInterface[] $rootNodes */
23 | $rootNodes = $model::find()->roots()->all();
24 | @@ -348,6 +348,9 @@ class Nestable extends Widget
25 | {
26 | /** @var ActiveRecord $model */
27 | $model = new $this->modelClass;
28 | + $newNodeString = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','New node');
29 | + $closeButtonString = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','Close');
30 | + $createNodeString = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','Create node');
31 |
32 | echo <<
34 | @@ -362,7 +365,7 @@ HTML;
35 | echo <<
37 |
38 | -
42 | HTML;
43 | @@ -372,8 +375,8 @@ HTML;
44 | echo <<
46 |
52 | HTML;
53 | $form->end();
54 | --
55 | 1.9.5.msysgit.0
56 |
57 |
--------------------------------------------------------------------------------
/src/interfaces/TreeInterface.php:
--------------------------------------------------------------------------------
1 | .dd-button {
39 | background: transparent;
40 | border: none;
41 | cursor: pointer;
42 | display: block;
43 | float: left;
44 | font-weight: bold;
45 | height: 34px;
46 | overflow: hidden;
47 | padding: 0;
48 | position: relative;
49 | text-align: center;
50 | text-indent: 100%;
51 | white-space: nowrap;
52 | width: 25px;
53 | line-height: 0px;
54 | }
55 |
56 | .dd-item > .dd-button:focus,
57 | .dd-item > .dd-edit:focus {
58 | outline: none;
59 | border: none;
60 | }
61 |
62 | .dd-item > .dd-button:before {
63 | content: '+';
64 | display: block;
65 | position: absolute;
66 | text-align: center;
67 | text-indent: 0;
68 | width: 100%;
69 | }
70 |
71 | .dd-item > .dd-button[data-action="collapse"]:before {
72 | content: '-';
73 | }
74 |
75 | .dd-placeholder,
76 | .dd-empty {
77 | background: #f2fbff;
78 | border: 1px dashed #b6bcbf;
79 | box-sizing: border-box;
80 | margin: 5px 0;
81 | min-height: 30px;
82 | moz-box-sizing: border-box;
83 | padding: 0;
84 | }
85 |
86 | .dd-empty {
87 | background-color: #e5e5e5;
88 | border: 1px dashed #bbb;
89 | min-height: 100px;
90 | }
91 |
92 | .dd-dragel {
93 | pointer-events: none;
94 | position: absolute;
95 | z-index: 9999;
96 | }
97 |
98 | .dd-dragel .dd-handle {
99 | background-color: #da4f49;
100 | background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f);
101 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));
102 | background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f);
103 | background-image: -o-linear-gradient(top, #ee5f5b, #bd362f);
104 | background-image: linear-gradient(to bottom, #ee5f5b, #bd362f);
105 | background-repeat: repeat-x;
106 | border-color: #bd362f #bd362f #802420;
107 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
108 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);
109 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
110 | }
111 |
112 | .dd-content {
113 | box-sizing: border-box;
114 | line-height: 20px;
115 | color: #333;
116 | display: block;
117 | font-weight: bold;
118 | height: 34px;
119 | margin: 5px 0;
120 | moz-box-sizing: border-box;
121 | padding: 5px 10px 5px 40px;
122 | text-decoration: none;
123 | cursor: pointer;
124 | background-color: #f5f5f5;
125 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
126 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
127 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
128 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
129 | background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
130 | background-repeat: repeat-x;
131 | border: 1px solid #cccccc;
132 | border-color: #e6e6e6 #e6e6e6 #bfbfbf;
133 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
134 | border-bottom-color: #b3b3b3;
135 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);
136 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
137 | }
138 |
139 | .dd-dragel > .dd-item > .dd-content {
140 | margin: 0 0 5px 0;
141 | }
142 |
143 | .dd-item > .dd-button {
144 | margin-left: 34px;
145 | }
146 |
147 | .dd-handle {
148 | cursor: pointer;
149 | left: 0;
150 | margin: 0;
151 | overflow: hidden;
152 | position: absolute;
153 | text-indent: 100%;
154 | top: 0;
155 | white-space: nowrap;
156 | line-height: 26px;
157 | width: 34px;
158 | height: 34px;
159 | background-color: #006dcc;
160 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
161 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
162 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
163 | background-image: -o-linear-gradient(top, #0088cc, #0044cc);
164 | background-image: linear-gradient(to bottom, #0088cc, #0044cc);
165 | background-repeat: repeat-x;
166 | border-color: #0044cc #0044cc #002a80;
167 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
168 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
169 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
170 | }
171 |
172 | .dd-handle:before {
173 | color: #fff;
174 | content: '≡';
175 | display: block;
176 | font-size: 20px;
177 | font-weight: normal;
178 | left: 0;
179 | position: absolute;
180 | text-align: center;
181 | text-indent: 0;
182 | top: 3px;
183 | width: 100%;
184 | }
185 |
186 | .dd-edit-panel {
187 | display: none;
188 | height: 63px;
189 | }
190 |
191 | .dd-input-name,
192 | .dd-input-url,
193 | .dd-input-bizrule {
194 | display: block;
195 | width: 100%;
196 | min-height: 30px;
197 | -webkit-box-sizing: border-box;
198 | -moz-box-sizing: border-box;
199 | box-sizing: border-box;
200 | }
201 |
202 | .dd-input-name,
203 | .dd-input-url {
204 | margin-bottom: 3px !important;
205 | }
206 |
--------------------------------------------------------------------------------
/src/widgets/nestable/Nestable.php:
--------------------------------------------------------------------------------
1 | id)) {
100 | $this->id = $this->getId();
101 | }
102 |
103 | if ($this->modelClass == null) {
104 | throw new InvalidConfigException('Param "modelClass" must be contain model name');
105 | }
106 |
107 | if (null == $this->behaviorName) {
108 | throw new InvalidConfigException("No 'behaviorName' supplied on action initialization.");
109 | }
110 |
111 | if (null == $this->advancedUpdateRoute && ($controller = Yii::$app->controller)) {
112 | $this->advancedUpdateRoute = "{$controller->id}/update";
113 | }
114 |
115 | if ($this->formFieldsCallable == null) {
116 | $this->formFieldsCallable = function ($form, $model) {
117 | /** @var ActiveForm $form */
118 | echo $form->field($model, $this->nameAttribute);
119 | };
120 | }
121 |
122 | /** @var ActiveRecord|TreeInterface $model */
123 | $model = $this->modelClass;
124 |
125 | /** @var ActiveRecord[]|TreeInterface[] $rootNodes */
126 | $rootNodes = $model::find()->roots()->all();
127 |
128 | if (!empty($rootNodes[0])) {
129 | /** @var ActiveRecord|TreeInterface $items */
130 | $items = $rootNodes[0]->populateTree();
131 | $this->_items = $this->prepareItems($items);
132 | }
133 | }
134 |
135 | /**
136 | * @param ActiveRecord|TreeInterface $node
137 | * @return array
138 | */
139 | protected function getNode($node)
140 | {
141 | $items = [];
142 | /** @var ActiveRecord[]|TreeInterface[] $children */
143 | $children = $node->children;
144 |
145 | foreach ($children as $n => $node) {
146 | $items[$n]['id'] = $node->getPrimaryKey();
147 | if(!is_string($node->{$this->nameAttribute}))
148 | throw new InvalidArgumentException("Value must be a string");
149 | $items[$n]['name'] = $node->{$this->nameAttribute};
150 | $items[$n]['children'] = $this->getNode($node);
151 | $items[$n]['update-url'] = Url::to([$this->advancedUpdateRoute, 'id' => $node->getPrimaryKey()]);
152 | }
153 |
154 | return $items;
155 | }
156 |
157 | /**
158 | * @param ActiveRecord|TreeInterface[] $node
159 | * @return array
160 | */
161 | private function prepareItems($node)
162 | {
163 | return $this->getNode($node);
164 | }
165 |
166 | /**
167 | * @param null $name
168 | * @return array
169 | */
170 | private function getPluginOptions($name = null)
171 | {
172 | $options = ArrayHelper::merge($this->getDefaultPluginOptions(), $this->pluginOptions);
173 |
174 | if (isset($options[$name])) {
175 | return $options[$name];
176 | }
177 |
178 | return $options;
179 | }
180 |
181 | /**
182 | * Работаем!
183 | */
184 | public function run()
185 | {
186 | $this->registerActionButtonsAssets();
187 | $this->actionButtons();
188 |
189 | Pjax::begin([
190 | 'id' => $this->id . '-pjax'
191 | ]);
192 | $this->registerPluginAssets();
193 | $this->renderMenu();
194 | $this->renderForm();
195 | Pjax::end();
196 |
197 | $this->actionButtons();
198 | }
199 |
200 | /**
201 | * Register Asset manager
202 | */
203 | private function registerPluginAssets()
204 | {
205 | NestableAsset::register($this->getView());
206 |
207 | $view = $this->getView();
208 |
209 | $pluginOptions = $this->getPluginOptions();
210 | $pluginOptions = Json::encode($pluginOptions);
211 | $view->registerJs("$('#{$this->id}').nestable({$pluginOptions});");
212 | // language=JavaScript
213 | $view->registerJs("
214 | $('#{$this->id}-new-node-form').on('beforeSubmit', function(e){
215 | $.ajax({
216 | url: '{$this->getPluginOptions('createUrl')}',
217 | method: 'POST',
218 | data: $(this).serialize(),
219 | success: function(data, textStatus, jqXHR) {
220 | $('#{$this->id}-new-node-modal').modal('hide')
221 | $.pjax.reload({container: '#{$this->id}-pjax'});
222 | window.scrollTo(0, document.body.scrollHeight);
223 | },
224 | }).fail(function (jqXHR) {
225 | alert(jqXHR.responseText);
226 | });
227 |
228 | return false;
229 | });
230 | ");
231 | }
232 |
233 | /**
234 | * Register Asset manager
235 | */
236 | private function registerActionButtonsAssets()
237 | {
238 | $view = $this->getView();
239 | $view->registerJs("
240 | $('.{$this->id}-nestable-menu [data-action]').on('click', function(e) {
241 | e.preventDefault();
242 |
243 | var target = $(e.target),
244 | action = target.data('action');
245 |
246 | switch (action) {
247 | case 'expand-all':
248 | $('#{$this->id}').nestable('expandAll');
249 | $('.{$this->id}-nestable-menu [data-action=\"expand-all\"]').hide();
250 | $('.{$this->id}-nestable-menu [data-action=\"collapse-all\"]').show();
251 |
252 | break;
253 | case 'collapse-all':
254 | $('#{$this->id}').nestable('collapseAll');
255 | $('.{$this->id}-nestable-menu [data-action=\"expand-all\"]').show();
256 | $('.{$this->id}-nestable-menu [data-action=\"collapse-all\"]').hide();
257 |
258 | break;
259 | }
260 | });
261 | ");
262 | }
263 |
264 | /**
265 | * Generate default plugin options
266 | * @return array
267 | */
268 | private function getDefaultPluginOptions()
269 | {
270 | $options = [
271 | 'namePlaceholder' => $this->getPlaceholderForName(),
272 | 'deleteAlert' => Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable',
273 | 'The nobe will be removed together with the children. Are you sure?'),
274 | 'newNodeTitle' => Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable',
275 | 'Enter the new node name'),
276 | ];
277 |
278 | $controller = Yii::$app->controller;
279 | if ($controller) {
280 | $options['moveUrl'] = Url::to(["{$controller->id}/moveNode"]);
281 | $options['createUrl'] = Url::to(["{$controller->id}/createNode"]);
282 | $options['updateUrl'] = Url::to(["{$controller->id}/updateNode"]);
283 | $options['deleteUrl'] = Url::to(["{$controller->id}/deleteNode"]);
284 | }
285 |
286 | if ($this->moveUrl) {
287 | $this->pluginOptions['moveUrl'] = $this->moveUrl;
288 | }
289 | if ($this->createUrl) {
290 | $this->pluginOptions['createUrl'] = $this->createUrl;
291 | }
292 | if ($this->updateUrl) {
293 | $this->pluginOptions['updateUrl'] = $this->updateUrl;
294 | }
295 | if ($this->deleteUrl) {
296 | $this->pluginOptions['deleteUrl'] = $this->deleteUrl;
297 | }
298 |
299 | return $options;
300 | }
301 |
302 | /**
303 | * Get placeholder for Name input
304 | */
305 | public function getPlaceholderForName()
306 | {
307 | return Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Node name');
308 | }
309 |
310 | /**
311 | * Кнопки действий над виджетом
312 | */
313 | public function actionButtons()
314 | {
315 | echo Html::beginTag('div', ['class' => "{$this->id}-nestable-menu"]);
316 |
317 | echo Html::beginTag('div', ['class' => 'btn-group']);
318 | echo Html::button(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Add node'), [
319 | 'data-toggle' => 'modal',
320 | 'data-target' => "#{$this->id}-new-node-modal",
321 | 'class' => 'btn btn-success'
322 | ]);
323 | echo Html::button(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Collapse all'), [
324 | 'data-action' => 'collapse-all',
325 | 'class' => 'btn btn-default'
326 | ]);
327 | echo Html::button(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Expand all'), [
328 | 'data-action' => 'expand-all',
329 | 'class' => 'btn btn-default',
330 | 'style' => 'display: none'
331 | ]);
332 | echo Html::endTag('div');
333 |
334 | echo Html::endTag('div');
335 | }
336 |
337 | /**
338 | * Вывод меню
339 | */
340 | private function renderMenu()
341 | {
342 | echo Html::beginTag('div', ['class' => 'dd-nestable', 'id' => $this->id]);
343 |
344 | $this->printLevel($this->_items);
345 |
346 | echo Html::endTag('div');
347 | }
348 |
349 | /**
350 | * Render form for new node
351 | */
352 | private function renderForm()
353 | {
354 | /** @var ActiveRecord $model */
355 | $model = new $this->modelClass;
356 | $labelNewNode = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','New node');
357 | $labelCloseButton = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','Close');
358 | $labelCreateNode = Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable','Create node');
359 |
360 | echo <<
362 |
363 |
364 | HTML;
365 | /** @var ActiveForm $form */
366 | $form = ActiveForm::begin([
367 | 'id' => $this->id . '-new-node-form'
368 | ]);
369 |
370 | echo <<
372 |
373 |
$labelNewNode
374 |
375 |
376 | HTML;
377 |
378 | echo call_user_func($this->formFieldsCallable, $form, $model);
379 |
380 | echo <<
382 |
386 | HTML;
387 | $form->end();
388 | echo <<
390 |
391 |
392 | HTML;
393 | }
394 |
395 | /**
396 | * Распечатка одного уровня
397 | * @param $level
398 | */
399 | protected function printLevel($level)
400 | {
401 | echo Html::beginTag('ol', ['class' => 'dd-list']);
402 |
403 | foreach ($level as $item) {
404 | $this->printItem($item);
405 | }
406 |
407 | echo Html::endTag('ol');
408 | }
409 |
410 | /**
411 | * Распечатка одного пункта
412 | * @param $item
413 | */
414 | protected function printItem($item)
415 | {
416 | $htmlOptions = ['class' => 'dd-item'];
417 | $htmlOptions['data-id'] = !empty($item['id']) ? $item['id'] : '';
418 |
419 | echo Html::beginTag('li', $htmlOptions);
420 |
421 | echo Html::tag('div', '', ['class' => 'dd-handle']);
422 | echo Html::tag('div', $item['name'], ['class' => 'dd-content']);
423 |
424 | echo Html::beginTag('div', ['class' => 'dd-edit-panel']);
425 | echo Html::input('text', null, $item['name'],
426 | ['class' => 'dd-input-name', 'placeholder' => $this->getPlaceholderForName()]);
427 |
428 | echo Html::beginTag('div', ['class' => 'btn-group']);
429 | echo Html::button(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Save'), [
430 | 'data-action' => 'save',
431 | 'class' => 'btn btn-success btn-sm',
432 | ]);
433 | echo Html::a(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Advanced editing'),
434 | $item['update-url'], [
435 | 'data-action' => 'advanced-editing',
436 | 'class' => 'btn btn-default btn-sm',
437 | 'target' => '_blank'
438 | ]);
439 | echo Html::button(Yii::t('vendor/voskobovich/yii2-tree-manager/widgets/nestable', 'Delete'), [
440 | 'data-action' => 'delete',
441 | 'class' => 'btn btn-danger btn-sm'
442 | ]);
443 | echo Html::endTag('div');
444 |
445 | echo Html::endTag('div');
446 |
447 | if (isset($item['children']) && count($item['children'])) {
448 | $this->printLevel($item['children']);
449 | }
450 |
451 | echo Html::endTag('li');
452 | }
453 | }
--------------------------------------------------------------------------------
/src/widgets/nestable/assets/jquery.nestable.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/
3 | * Dual-licensed under the BSD or MIT licenses
4 | */
5 | ;
6 | (function ($, window, document, undefined) {
7 | var hasTouch = 'ontouchstart' in window;
8 |
9 | /**
10 | * Detect CSS pointer-events property
11 | * events are normally disabled on the dragging element to avoid conflicts
12 | * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
13 | */
14 | var hasPointerEvents = (function () {
15 | var el = document.createElement('div'),
16 | docEl = document.documentElement;
17 | if (!('pointerEvents' in el.style)) {
18 | return false;
19 | }
20 | el.style.pointerEvents = 'auto';
21 | el.style.pointerEvents = 'x';
22 | docEl.appendChild(el);
23 | var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto';
24 | docEl.removeChild(el);
25 | return !!supports;
26 | })();
27 |
28 | var eStart = hasTouch ? 'touchstart' : 'mousedown',
29 | eMove = hasTouch ? 'touchmove' : 'mousemove',
30 | eEnd = hasTouch ? 'touchend' : 'mouseup',
31 | eCancel = hasTouch ? 'touchcancel' : 'mouseup';
32 |
33 | var defaults = {
34 | listNodeName: 'ol',
35 | itemNodeName: 'li',
36 | rootClass: 'dd-nestable',
37 | contentClass: 'dd-content',
38 | editPanelClass: 'dd-edit-panel',
39 | listClass: 'dd-list',
40 | itemClass: 'dd-item',
41 | dragClass: 'dd-dragel',
42 | inputOpenClass: 'dd-open',
43 | handleClass: 'dd-handle',
44 | collapsedClass: 'dd-collapsed',
45 | placeClass: 'dd-placeholder',
46 | inputNameClass: 'dd-input-name',
47 | noDragClass: 'dd-nodrag',
48 | emptyClass: 'dd-empty',
49 | btnGroupClass: 'btn-group',
50 | expandBtnHTML: '
',
51 | collapseBtnHTML: '
',
52 | group: 0,
53 | maxDepth: 5,
54 | threshold: 20,
55 | moveUrl: '',
56 | updateUrl: '',
57 | deleteUrl: '',
58 | namePlaceholder: '',
59 | deleteAlert: 'The nobe will be removed together with the children. Are you sure?',
60 | newNodeTitle: 'Enter the new node name'
61 | };
62 |
63 | function Plugin(element, options) {
64 | this.w = $(window);
65 | this.el = $(element);
66 | this.options = $.extend({}, defaults, options);
67 | this.init();
68 | }
69 |
70 | Plugin.prototype = {
71 |
72 | init: function () {
73 | var tree = this;
74 |
75 | tree.reset();
76 |
77 | tree.el.data('nestable-group', this.options.group);
78 |
79 | tree.placeEl = $('
');
80 |
81 | $.each(this.el.find(tree.options.itemNodeName), function (k, el) {
82 | // Вставляем иконки открытия\закрытия дочек
83 | tree.setParent($(el));
84 | });
85 |
86 | // Вешаем эвенты клика для открытия панели редактирования
87 | tree.setPanelEvents(tree.el);
88 | tree.setActionButtonsEvents(tree.el);
89 |
90 | tree.el.on('click', 'button', function (e) {
91 | if (tree.dragEl || (!hasTouch && e.button !== 0)) {
92 | return;
93 | }
94 | var target = $(e.currentTarget),
95 | action = target.data('action'),
96 | item = target.parent(tree.options.itemNodeName);
97 | if (action === 'collapse') {
98 | tree.collapseItem(item);
99 | }
100 | if (action === 'expand') {
101 | tree.expandItem(item);
102 | }
103 | });
104 |
105 | var onStartEvent = function (e) {
106 | var handle = $(e.target);
107 | if (!handle.hasClass(tree.options.handleClass)) {
108 | if (handle.closest('.' + tree.options.noDragClass).length) {
109 | return;
110 | }
111 | handle = handle.closest('.' + tree.options.handleClass);
112 | }
113 | if (!handle.length || tree.dragEl || (!hasTouch && e.button !== 0) || (hasTouch && e.touches.length !== 1)) {
114 | return;
115 | }
116 | e.preventDefault();
117 | tree.dragStart(hasTouch ? e.touches[0] : e);
118 | };
119 |
120 | var onMoveEvent = function (e) {
121 | if (tree.dragEl) {
122 | e.preventDefault();
123 | tree.dragMove(hasTouch ? e.touches[0] : e);
124 | }
125 | };
126 |
127 | var onEndEvent = function (e) {
128 | if (tree.dragEl) {
129 | e.preventDefault();
130 | tree.dragStop(hasTouch ? e.touches[0] : e);
131 | }
132 | };
133 |
134 | if (hasTouch) {
135 | tree.el[0].addEventListener(eStart, onStartEvent, false);
136 | window.addEventListener(eMove, onMoveEvent, false);
137 | window.addEventListener(eEnd, onEndEvent, false);
138 | window.addEventListener(eCancel, onEndEvent, false);
139 | } else {
140 | tree.el.on(eStart, onStartEvent);
141 | tree.w.on(eMove, onMoveEvent);
142 | tree.w.on(eEnd, onEndEvent);
143 | }
144 | },
145 |
146 | /**
147 | * Вешаем onClick на тело пункта
148 | * @returns {*}
149 | */
150 | setPanelEvents: function (el) {
151 | var tree = this;
152 |
153 | el.on('keyup', '.' + tree.options.inputNameClass, function (e) {
154 | var target = $(e.target),
155 | li = target.closest('.' + tree.options.itemClass),
156 | content = li.children('.' + tree.options.contentClass);
157 |
158 | content.html(target.val());
159 | });
160 |
161 | el.on('click', '.' + tree.options.contentClass, function (e) {
162 | var owner = $(e.target).parent();
163 | var editPanel = owner.children('.' + tree.options.editPanelClass);
164 |
165 | if (!editPanel.hasClass(tree.options.inputOpenClass)) {
166 | $('.' + tree.options.editPanelClass)
167 | .slideUp(100)
168 | .removeClass(tree.options.inputOpenClass);
169 |
170 | editPanel.addClass(tree.options.inputOpenClass);
171 | editPanel.slideDown(100);
172 | }
173 | else {
174 | editPanel.removeClass(tree.options.inputOpenClass);
175 | editPanel.slideUp(100);
176 | }
177 | });
178 | },
179 |
180 | /**
181 | * Вешаем onClick на кнопки управления нодой
182 | * @returns {*}
183 | */
184 | setActionButtonsEvents: function (el) {
185 | var tree = this;
186 |
187 | el.on('click', '.' + tree.options.btnGroupClass + ' [data-action="save"]', function (e) {
188 | var target = $(e.target),
189 | li = target.closest('.' + tree.options.itemClass);
190 |
191 | tree.updateNodeRequest(li);
192 | });
193 |
194 | el.on('click', '.' + tree.options.btnGroupClass + ' [data-action="delete"]', function (e) {
195 | var target = $(e.target),
196 | li = target.closest('.' + tree.options.itemClass);
197 |
198 | if (confirm(tree.options.deleteAlert)) {
199 | tree.deleteNodeRequest(li);
200 | }
201 | });
202 | },
203 |
204 | reset: function () {
205 | this.mouse = {
206 | offsetX: 0,
207 | offsetY: 0,
208 | startX: 0,
209 | startY: 0,
210 | lastX: 0,
211 | lastY: 0,
212 | nowX: 0,
213 | nowY: 0,
214 | distX: 0,
215 | distY: 0,
216 | dirAx: 0,
217 | dirX: 0,
218 | dirY: 0,
219 | lastDirX: 0,
220 | lastDirY: 0,
221 | distAxX: 0,
222 | distAxY: 0
223 | };
224 | this.moving = false;
225 | this.dragEl = null;
226 | this.dragRootEl = null;
227 | this.dragDepth = 0;
228 | this.hasNewRoot = false;
229 | this.pointEl = null;
230 | },
231 |
232 | expandItem: function (li) {
233 | li.removeClass(this.options.collapsedClass);
234 | li.children('[data-action="expand"]').hide();
235 | li.children('[data-action="collapse"]').show();
236 | li.children(this.options.listNodeName).show();
237 | },
238 |
239 | collapseItem: function (li) {
240 | var lists = li.children(this.options.listNodeName);
241 | if (lists.length) {
242 | li.addClass(this.options.collapsedClass);
243 | li.children('[data-action="collapse"]').hide();
244 | li.children('[data-action="expand"]').show();
245 | li.children(this.options.listNodeName).hide();
246 | }
247 | },
248 |
249 | expandAll: function () {
250 | var tree = this;
251 | tree.el.find(tree.options.itemNodeName).each(function () {
252 | tree.expandItem($(this));
253 | });
254 | },
255 |
256 | collapseAll: function () {
257 | var tree = this;
258 | tree.el.find(tree.options.itemNodeName).each(function () {
259 | tree.collapseItem($(this));
260 | });
261 | },
262 |
263 | setParent: function (li) {
264 | if (li.children(this.options.listNodeName).length) {
265 | li.prepend($(this.options.expandBtnHTML));
266 | li.prepend($(this.options.collapseBtnHTML));
267 | }
268 | li.children('[data-action="expand"]').hide();
269 | },
270 |
271 | unsetParent: function (li) {
272 | li.removeClass(this.options.collapsedClass);
273 | li.children('[data-action]').remove();
274 | li.children(this.options.listNodeName).remove();
275 | },
276 |
277 | dragStart: function (e) {
278 | var mouse = this.mouse,
279 | target = $(e.target),
280 | dragItem = target.closest(this.options.itemNodeName);
281 |
282 | this.placeEl.css('height', dragItem.height());
283 |
284 | mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left;
285 | mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top;
286 | mouse.startX = mouse.lastX = e.pageX;
287 | mouse.startY = mouse.lastY = e.pageY;
288 |
289 | this.dragRootEl = this.el;
290 |
291 | this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass);
292 | this.dragEl.css('width', dragItem.width());
293 |
294 | // fix for zepto.js
295 | //dragItem.after(this.placeEl).detach().appendTo(this.dragEl);
296 | dragItem.after(this.placeEl);
297 | dragItem[0].parentNode.removeChild(dragItem[0]);
298 | dragItem.appendTo(this.dragEl);
299 |
300 | $(document.body).append(this.dragEl);
301 | this.dragEl.css({
302 | 'left': e.pageX - mouse.offsetX,
303 | 'top': e.pageY - mouse.offsetY
304 | });
305 | // total depth of dragging item
306 | var i, depth,
307 | items = this.dragEl.find(this.options.itemNodeName);
308 | for (i = 0; i < items.length; i++) {
309 | depth = $(items[i]).parents(this.options.listNodeName).length;
310 | if (depth > this.dragDepth) {
311 | this.dragDepth = depth;
312 | }
313 | }
314 | },
315 |
316 | dragStop: function (e) {
317 | // fix for zepto.js
318 | //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach());
319 | var el = this.dragEl.children(this.options.itemNodeName).first();
320 | el[0].parentNode.removeChild(el[0]);
321 | this.placeEl.replaceWith(el);
322 |
323 | this.dragEl.remove();
324 |
325 | this.moveNodeRequest(el);
326 |
327 | this.el.trigger('change');
328 | if (this.hasNewRoot) {
329 | this.dragRootEl.trigger('change');
330 | }
331 | this.reset();
332 | },
333 |
334 | /**
335 | * Save new node position on server
336 | * @param el
337 | */
338 | moveNodeRequest: function (el) {
339 | var tree = this;
340 | var id = el.data('id');
341 | if (typeof id === "undefined" || !id) {
342 | return false;
343 | }
344 |
345 | var prev = el.prev(this.options.itemNodeName);
346 | var next = el.next(this.options.itemNodeName);
347 | var parent = el.parents(this.options.itemNodeName);
348 |
349 | $.ajax({
350 | url: this.options.moveUrl + '?id=' + id,
351 | method: 'POST',
352 | context: document.body,
353 | data: {
354 | parent_id: $(parent).data('id'),
355 | prev_id: (prev.length ? prev.data('id') : 0),
356 | next_id: (next.length ? next.data('id') : 0)
357 | },
358 | success: function () {
359 | $.pjax.reload({container: '#' + tree.el.attr('id') + '-pjax'});
360 | },
361 | }).fail(function (jqXHR) {
362 | alert(jqXHR.responseText);
363 | });
364 | },
365 |
366 | deleteNodeRequest: function (el) {
367 | var tree = this;
368 | var id = el.data('id');
369 | if (typeof id === "undefined" || !id) {
370 | return false;
371 | }
372 |
373 | $.ajax({
374 | url: this.options.deleteUrl + '?id=' + id,
375 | method: 'POST',
376 | context: document.body,
377 | success: function () {
378 | $.pjax.reload({container: '#' + tree.el.attr('id') + '-pjax'});
379 | },
380 | }).fail(function (jqXHR) {
381 | alert(jqXHR.responseText);
382 | });
383 | },
384 |
385 | updateNodeRequest: function (el) {
386 | var tree = this,
387 | id = el.data('id');
388 |
389 | if (typeof id === "undefined" || !id) {
390 | return false;
391 | }
392 |
393 | var name = el.find('.' + this.options.inputNameClass);
394 |
395 | $.ajax({
396 | url: this.options.updateUrl + '?id=' + id,
397 | method: 'POST',
398 | context: document.body,
399 | data: {
400 | name: name.val()
401 | },
402 | success: function () {
403 | var editPanel = el.children('.' + tree.options.editPanelClass);
404 | editPanel.removeClass(tree.options.inputOpenClass);
405 | editPanel.slideUp(100);
406 | },
407 | }).fail(function (jqXHR) {
408 | alert(jqXHR.responseText);
409 | });
410 | },
411 |
412 | dragMove: function (e) {
413 | var tree, parent, prev, next, depth,
414 | opt = this.options,
415 | mouse = this.mouse;
416 |
417 | this.dragEl.css({
418 | 'left': e.pageX - mouse.offsetX,
419 | 'top': e.pageY - mouse.offsetY
420 | });
421 |
422 | // mouse position last events
423 | mouse.lastX = mouse.nowX;
424 | mouse.lastY = mouse.nowY;
425 | // mouse position this events
426 | mouse.nowX = e.pageX;
427 | mouse.nowY = e.pageY;
428 | // distance mouse moved between events
429 | mouse.distX = mouse.nowX - mouse.lastX;
430 | mouse.distY = mouse.nowY - mouse.lastY;
431 | // direction mouse was moving
432 | mouse.lastDirX = mouse.dirX;
433 | mouse.lastDirY = mouse.dirY;
434 | // direction mouse is now moving (on both axis)
435 | mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
436 | mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
437 | // axis mouse is now moving on
438 | var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
439 |
440 | // do nothing on first move
441 | if (!mouse.moving) {
442 | mouse.dirAx = newAx;
443 | mouse.moving = true;
444 | return;
445 | }
446 |
447 | // calc distance moved on this axis (and direction)
448 | if (mouse.dirAx !== newAx) {
449 | mouse.distAxX = 0;
450 | mouse.distAxY = 0;
451 | } else {
452 | mouse.distAxX += Math.abs(mouse.distX);
453 | if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
454 | mouse.distAxX = 0;
455 | }
456 | mouse.distAxY += Math.abs(mouse.distY);
457 | if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
458 | mouse.distAxY = 0;
459 | }
460 | }
461 | mouse.dirAx = newAx;
462 |
463 | /**
464 | * move horizontal
465 | */
466 | if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
467 | // reset move distance on x-axis for new phase
468 | mouse.distAxX = 0;
469 | prev = this.placeEl.prev(opt.itemNodeName);
470 | // increase horizontal level if previous sibling exists and is not collapsed
471 | if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) {
472 | // cannot increase level when item above is collapsed
473 | tree = prev.find(opt.listNodeName).last();
474 | // check if depth limit has reached
475 | depth = this.placeEl.parents(opt.listNodeName).length;
476 | if (depth + this.dragDepth <= opt.maxDepth) {
477 | // create new sub-level if one doesn't exist
478 | if (!tree.length) {
479 | tree = $('<' + opt.listNodeName + '/>').addClass(opt.listClass);
480 | tree.append(this.placeEl);
481 | prev.append(tree);
482 | this.setParent(prev);
483 | } else {
484 | // else append to next level up
485 | tree = prev.children(opt.listNodeName).last();
486 | tree.append(this.placeEl);
487 | }
488 | }
489 | }
490 | // decrease horizontal level
491 | if (mouse.distX < 0) {
492 | // we can't decrease a level if an item preceeds the current one
493 | next = this.placeEl.next(opt.itemNodeName);
494 | if (!next.length) {
495 | parent = this.placeEl.parent();
496 | this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
497 | if (!parent.children().length) {
498 | this.unsetParent(parent.parent());
499 | }
500 | }
501 | }
502 | }
503 |
504 | var isEmpty = false;
505 |
506 | // find list item under cursor
507 | if (!hasPointerEvents) {
508 | this.dragEl[0].style.visibility = 'hidden';
509 | }
510 | this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
511 | if (!hasPointerEvents) {
512 | this.dragEl[0].style.visibility = 'visible';
513 | }
514 | if (this.pointEl.hasClass(opt.handleClass)) {
515 | this.pointEl = this.pointEl.parent(opt.itemNodeName);
516 | }
517 | if (this.pointEl.hasClass(opt.emptyClass)) {
518 | isEmpty = true;
519 | }
520 | else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
521 | return;
522 | }
523 |
524 | // find parent list of item under cursor
525 | var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
526 | isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
527 |
528 | /**
529 | * move vertical
530 | */
531 | if (!mouse.dirAx || isNewRoot || isEmpty) {
532 | // check if groups match if dragging over new root
533 | if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
534 | return;
535 | }
536 | // check depth limit
537 | depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
538 | if (depth > opt.maxDepth) {
539 | return;
540 | }
541 | var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
542 | parent = this.placeEl.parent();
543 | // if empty create new list to replace empty placeholder
544 | if (isEmpty) {
545 | tree = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
546 | tree.append(this.placeEl);
547 | this.pointEl.replaceWith(tree);
548 | }
549 | else if (before) {
550 | this.pointEl.before(this.placeEl);
551 | }
552 | else {
553 | this.pointEl.after(this.placeEl);
554 | }
555 | if (!parent.children().length) {
556 | this.unsetParent(parent.parent());
557 | }
558 | if (!this.dragRootEl.find(opt.itemNodeName).length) {
559 | this.dragRootEl.append('
');
560 | }
561 | // parent root list has changed
562 | if (isNewRoot) {
563 | this.dragRootEl = pointElRoot;
564 | this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
565 | }
566 | }
567 | }
568 |
569 | };
570 |
571 | $.fn.nestable = function (params) {
572 | var lists = this,
573 | retval = this;
574 |
575 | lists.each(function () {
576 | var plugin = $(this).data("nestable");
577 |
578 | if (!plugin) {
579 | $(this).data("nestable", new Plugin(this, params));
580 | $(this).data("nestable-id", new Date().getTime());
581 | } else {
582 | if (typeof params === 'string' && typeof plugin[params] === 'function') {
583 | retval = plugin[params]();
584 | }
585 | }
586 | });
587 |
588 | return retval || lists;
589 | };
590 |
591 | })(window.jQuery || window.Zepto, window, document);
592 |
--------------------------------------------------------------------------------