├── .gitignore
├── LICENSE
├── README.md
├── composer.json
└── src
├── actions
├── BaseAction.php
├── CreateNodeAction.php
├── DeleteNodeAction.php
├── MoveNodeAction.php
└── UpdateNodeAction.php
├── behaviors
└── NestedSetsBehavior.php
├── forms
└── MoveNodeForm.php
└── widgets
└── nestable
├── Nestable.php
├── NestableAsset.php
└── assets
├── jquery.nestable.css
└── jquery.nestable.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendors
3 | composer.lock
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Yii2 Nested Sets Editor
2 | ===
3 |
4 | This behavior soon will be **DEPRECATED**.
5 | See the new version [**Yii2 Tree Manager**](https://github.com/voskobovich/yii2-tree-manager).
6 |
7 | ## About
8 | Editor nested set using jquery.nestable plugin.
9 |
10 | Реализует полный набор CRUD операций для узлов дерева.
11 |
12 | Внимание!
13 | ---
14 | Есть улучшеная версия пакета для управление деревом - [yii2-tree-manager](https://github.com/voskobovich/yii2-tree-manager).
15 |
16 | Installation
17 | -------------
18 |
19 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
20 |
21 | Either run
22 |
23 | ```
24 | php composer.phar require --prefer-dist voskobovich/yii2-nested-sets-editor "~1.0.0"
25 | ```
26 |
27 | or add
28 |
29 | ```
30 | "voskobovich/yii2-nested-sets-editor": "~1.0.0"
31 | ```
32 |
33 | to the require section of your `composer.json` file.
34 |
35 |
36 | Внимание!
37 | -----
38 | В расширении наследуется и расширяется behavior [Nested Sets Behavior for Yii 2](https://github.com/creocoder/yii2-nested-sets).
39 | Всю информацию по настройке поведения можно взять на [странице](https://github.com/creocoder/yii2-nested-sets).
40 |
41 | Но для работы виджета нужно использовать реализацию поведения из этого пакета!
42 |
43 |
44 | Usage
45 | -----
46 | 1. Подключите behavior из этого пакета к своей модели и сконфигурируйте как сказано в [документации](https://github.com/creocoder/yii2-nested-sets).
47 | ```
48 | public function behaviors()
49 | {
50 | return [
51 | 'nestedSetsBehavior' => 'voskobovich\nestedsets\behaviors\NestedSetsBehavior',
52 | ];
53 | }
54 | ```
55 | 2. Подключите в контроллер дополнительные actions
56 | ```
57 | public function actions()
58 | {
59 | return [
60 | 'moveNode' => [
61 | 'class' => 'voskobovich\nestedsets\actions\MoveNodeAction',
62 | 'modelClass' => 'models\ModelName',
63 | ],
64 | 'deleteNode' => [
65 | 'class' => 'voskobovich\nestedsets\actions\DeleteNodeAction',
66 | 'modelClass' => 'models\ModelName',
67 | ],
68 | 'updateNode' => [
69 | 'class' => 'voskobovich\nestedsets\actions\UpdateNodeAction',
70 | 'modelClass' => 'models\ModelName',
71 | ],
72 | 'createNode' => [
73 | 'class' => 'voskobovich\nestedsets\actions\CreateNodeAction',
74 | 'modelClass' => 'models\ModelName',
75 | ],
76 | ];
77 | }
78 | ```
79 | 3. Выведите виджет в удобном месте
80 | ```
81 | = \voskobovich\nestedsets\widgets\nestable\Nestable::widget([
82 | 'modelClass' => 'models\ModelName',
83 | ]) ?>
84 | ```
85 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voskobovich/yii2-nested-sets-editor",
3 | "description": "Nested set editor 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-nested-sets-editor",
12 | "type": "yii2-widget",
13 | "license": "MIT",
14 | "support": {
15 | "issues": "https://github.com/voskobovich/yii2-nested-sets-editor/issues",
16 | "source": "https://github.com/voskobovich/yii2-nested-sets-editor"
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 | "creocoder/yii2-nested-sets": "~0.9.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "voskobovich\\nestedsets\\actions\\": "src/actions",
34 | "voskobovich\\nestedsets\\behaviors\\": "src/behaviors",
35 | "voskobovich\\nestedsets\\forms\\": "src/forms",
36 | "voskobovich\\nestedsets\\widgets\\nestable\\": "src/widgets/nestable"
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/actions/BaseAction.php:
--------------------------------------------------------------------------------
1 | modelClass) {
31 | throw new InvalidConfigException('Param "modelClass" must be contain model name with namespace.');
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/actions/CreateNodeAction.php:
--------------------------------------------------------------------------------
1 | request->post();
30 |
31 | /** @var ActiveRecord|NestedSetsBehavior $model */
32 | $model = new $this->modelClass;
33 | $model->load($post);
34 |
35 | if ($model->validate()) {
36 | $roots = $model::find()->roots()->all();
37 |
38 | if (isset($roots[0])) {
39 | $model->appendTo($roots[0]);
40 | } else {
41 | $model->makeRoot();
42 | }
43 | }
44 |
45 | return null;
46 | }
47 | }
--------------------------------------------------------------------------------
/src/actions/DeleteNodeAction.php:
--------------------------------------------------------------------------------
1 | modelClass;
27 |
28 | /*
29 | * Locate the supplied model, left, right and parent models
30 | */
31 | $pkAttribute = $model->getTableSchema()->primaryKey[0];
32 |
33 | /** @var ActiveRecord|NestedSetsBehavior $model */
34 | $model = $model::find()->where([$pkAttribute => $id])->one();
35 |
36 | if ($model == null) {
37 | throw new NotFoundHttpException('Node not found');
38 | }
39 |
40 | $model->deleteWithChildren();
41 |
42 | return null;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/actions/MoveNodeAction.php:
--------------------------------------------------------------------------------
1 | request->post();
32 |
33 | $form = new MoveNodeForm();
34 | $form->id = $id;
35 | $form->setAttributes($params);
36 |
37 | if (!$form->validate()) {
38 | throw new BadRequestHttpException();
39 | }
40 |
41 | $form->moveNode($this->modelClass, $this->behaviorName);
42 |
43 | return null;
44 | }
45 | }
--------------------------------------------------------------------------------
/src/actions/UpdateNodeAction.php:
--------------------------------------------------------------------------------
1 | modelClass;
37 |
38 | /*
39 | * Locate the supplied model, left, right and parent models
40 | */
41 | $pkAttribute = $model->getTableSchema()->primaryKey[0];
42 |
43 | /** @var ActiveRecord|NestedSetsBehavior $model */
44 | $model = $model::find()->where([$pkAttribute => $id])->one();
45 |
46 | if ($model == null) {
47 | throw new NotFoundHttpException('Node not found');
48 | }
49 |
50 | $name = Yii::$app->request->post('name');
51 | $model->{$this->nameAttribute} = $name;
52 | if (!$model->validate()) {
53 | throw new HttpException($model->getFirstError($this->nameAttribute));
54 | }
55 | $model->update(true, [$this->nameAttribute]);
56 |
57 | return null;
58 | }
59 | }
--------------------------------------------------------------------------------
/src/behaviors/NestedSetsBehavior.php:
--------------------------------------------------------------------------------
1 | node = $this->owner;
20 | parent::moveNode($value, $depth);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/forms/MoveNodeForm.php:
--------------------------------------------------------------------------------
1 | getBehavior($behaviorName);
58 |
59 | if ($behavior == null) {
60 | throw new InvalidConfigException('Behavior "' . $behaviorName . '" not found');
61 | }
62 |
63 | if (!$behavior instanceof NestedSetsBehavior) {
64 | throw new InvalidConfigException('Behavior must be implemented "voskobovich\nestedsets\behaviors\NestedSetsBehavior"');
65 | }
66 |
67 | /*
68 | * Locate the supplied model, left, right and parent models
69 | */
70 | $pkAttribute = $model->getTableSchema()->primaryKey[0];
71 |
72 | /** @var ActiveRecord|NestedSetsBehavior $currentModel */
73 | $currentModel = $model::find()->where([$pkAttribute => $this->id])->one();
74 | $lftModel = $model::find()->where([$pkAttribute => $this->left])->one();
75 | $rgtModel = $model::find()->where([$pkAttribute => $this->right])->one();
76 | $parentModel = $model::find()->where([$pkAttribute => $this->parent])->one();
77 |
78 | /*
79 | * Calculate the depth change
80 | */
81 | if (null == $parentModel) {
82 | $depthDelta = -1;
83 | } else if (null == ($parent = $currentModel->parents(1)->one())) {
84 | $depthDelta = 0;
85 | } else if ($parent->getPrimaryKey() != $parentModel->getPrimaryKey()) {
86 | $depthDelta = $parentModel->{$behavior->depthAttribute} - $currentModel->{$behavior->depthAttribute} + 1;
87 | } else {
88 | $depthDelta = 0;
89 | }
90 |
91 | /*
92 | * Calculate the left/right change
93 | */
94 | if (null == $lftModel) {
95 | $currentModel->moveNode((($parentModel ? $parentModel->{$behavior->leftAttribute} : 0) + 1), $depthDelta);
96 | } else if (null == $rgtModel) {
97 | $currentModel->moveNode((($lftModel ? $lftModel->{$behavior->rightAttribute} : 0) + 1), $depthDelta);
98 | } else {
99 | $currentModel->moveNode(($rgtModel ? $rgtModel->{$behavior->leftAttribute} : 0), $depthDelta);
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/src/widgets/nestable/Nestable.php:
--------------------------------------------------------------------------------
1 | id)) {
109 | $this->id = $this->getId();
110 | }
111 |
112 | if ($this->modelClass == null) {
113 | throw new InvalidConfigException('Param "modelClass" must be contain model name');
114 | }
115 |
116 | if (null == $this->behaviorName) {
117 | throw new InvalidConfigException("No 'behaviorName' supplied on action initialization.");
118 | }
119 |
120 | if (null == $this->advancedUpdateRoute && ($controller = Yii::$app->controller)) {
121 | $this->advancedUpdateRoute = "{$controller->id}/update";
122 | }
123 |
124 | if ($this->formFieldsCallable == null) {
125 | $this->formFieldsCallable = function ($form, $model) {
126 | /** @var ActiveForm $form */
127 | echo $form->field($model, $this->nameAttribute);
128 | };
129 | }
130 |
131 | /** @var ActiveRecord $model */
132 | $model = new $this->modelClass;
133 | /** @var NestedSetsBehavior $behavior */
134 | $behavior = $model->getBehavior($this->behaviorName);
135 |
136 | $this->_leftAttribute = $behavior->leftAttribute;
137 | $this->_rightAttribute = $behavior->rightAttribute;
138 |
139 | $items = $model::find()
140 | ->orderBy([$this->_leftAttribute => SORT_ASC])
141 | ->all();
142 | $this->_items = $this->prepareItems($items);
143 | }
144 |
145 | /**
146 | * @param ActiveRecord[] $items
147 | * @return array
148 | */
149 | private function prepareItems($items)
150 | {
151 | $stack = [];
152 | $arraySet = [];
153 |
154 | foreach ($items as $item) {
155 | $stackSize = count($stack);
156 | while ($stackSize > 0 && $stack[$stackSize - 1]['rgt'] < $item->{$this->_leftAttribute}) {
157 | array_pop($stack);
158 | $stackSize--;
159 | }
160 |
161 | $link =& $arraySet;
162 | for ($i = 0; $i < $stackSize; $i++) {
163 | $link =& $link[$stack[$i]['index']]['children']; //navigate to the proper children array
164 | }
165 | $tmp = array_push($link, [
166 | 'id' => $item->getPrimaryKey(),
167 | 'name' => $item->{$this->nameAttribute},
168 | 'update-url' => Url::to([$this->advancedUpdateRoute, 'id' => $item->getPrimaryKey()]),
169 | 'children' => []
170 | ]);
171 | array_push($stack, [
172 | 'index' => $tmp - 1,
173 | 'rgt' => $item->{$this->_rightAttribute}
174 | ]);
175 | }
176 |
177 | return $arraySet;
178 | }
179 |
180 | /**
181 | * @param null $name
182 | * @return array
183 | */
184 | private function getPluginOptions($name = null)
185 | {
186 | $options = ArrayHelper::merge($this->getDefaultPluginOptions(), $this->pluginOptions);
187 |
188 | if (isset($options[$name])) {
189 | return $options[$name];
190 | }
191 |
192 | return $options;
193 | }
194 |
195 | /**
196 | * Работаем!
197 | */
198 | public function run()
199 | {
200 | $this->registerActionButtonsAssets();
201 | $this->actionButtons();
202 |
203 | Pjax::begin([
204 | 'id' => $this->id . '-pjax'
205 | ]);
206 | $this->registerPluginAssets();
207 | $this->renderMenu();
208 | $this->renderForm();
209 | Pjax::end();
210 |
211 | $this->actionButtons();
212 | }
213 |
214 | /**
215 | * Register Asset manager
216 | */
217 | private function registerPluginAssets()
218 | {
219 | NestableAsset::register($this->getView());
220 |
221 | $view = $this->getView();
222 |
223 | $pluginOptions = $this->getPluginOptions();
224 | $pluginOptions = Json::encode($pluginOptions);
225 | $view->registerJs("$('#{$this->id}').nestable({$pluginOptions});");
226 | $view->registerJs("
227 | $('#{$this->id}-new-node-form').on('beforeSubmit', function(e){
228 | $.ajax({
229 | url: '{$this->getPluginOptions('createUrl')}',
230 | method: 'POST',
231 | data: $(this).serialize()
232 | }).success(function (data, textStatus, jqXHR) {
233 | $('#{$this->id}-new-node-modal').modal('hide')
234 | $.pjax.reload({container: '#{$this->id}-pjax'});
235 | window.scrollTo(0, document.body.scrollHeight);
236 | }).fail(function (jqXHR) {
237 | alert(jqXHR.responseText);
238 | });
239 |
240 | return false;
241 | });
242 | ");
243 | }
244 |
245 | /**
246 | * Register Asset manager
247 | */
248 | private function registerActionButtonsAssets()
249 | {
250 | $view = $this->getView();
251 | $view->registerJs("
252 | $('.{$this->id}-nestable-menu [data-action]').on('click', function(e) {
253 | e.preventDefault();
254 |
255 | var target = $(e.target),
256 | action = target.data('action');
257 |
258 | switch (action) {
259 | case 'expand-all':
260 | $('#{$this->id}').nestable('expandAll');
261 | $('.{$this->id}-nestable-menu [data-action=\"expand-all\"]').hide();
262 | $('.{$this->id}-nestable-menu [data-action=\"collapse-all\"]').show();
263 |
264 | break;
265 | case 'collapse-all':
266 | $('#{$this->id}').nestable('collapseAll');
267 | $('.{$this->id}-nestable-menu [data-action=\"expand-all\"]').show();
268 | $('.{$this->id}-nestable-menu [data-action=\"collapse-all\"]').hide();
269 |
270 | break;
271 | }
272 | });
273 | ");
274 | }
275 |
276 | /**
277 | * Generate default plugin options
278 | * @return array
279 | */
280 | private function getDefaultPluginOptions()
281 | {
282 | $options = [
283 | 'namePlaceholder' => $this->getPlaceholderForName(),
284 | 'deleteAlert' => Yii::t('voskobovich/nestedsets',
285 | 'The nobe will be removed together with the children. Are you sure?'),
286 | 'newNodeTitle' => Yii::t('voskobovich/nestedsets', 'Enter the new node name'),
287 | ];
288 |
289 | $controller = Yii::$app->controller;
290 | if ($controller) {
291 | $options['moveUrl'] = Url::to(["{$controller->id}/moveNode"]);
292 | $options['createUrl'] = Url::to(["{$controller->id}/createNode"]);
293 | $options['updateUrl'] = Url::to(["{$controller->id}/updateNode"]);
294 | $options['deleteUrl'] = Url::to(["{$controller->id}/deleteNode"]);
295 | }
296 |
297 | if ($this->moveUrl) {
298 | $this->pluginOptions['moveUrl'] = $this->moveUrl;
299 | }
300 | if ($this->createUrl) {
301 | $this->pluginOptions['createUrl'] = $this->createUrl;
302 | }
303 | if ($this->updateUrl) {
304 | $this->pluginOptions['updateUrl'] = $this->updateUrl;
305 | }
306 | if ($this->deleteUrl) {
307 | $this->pluginOptions['deleteUrl'] = $this->deleteUrl;
308 | }
309 |
310 | return $options;
311 | }
312 |
313 | /**
314 | * Get placeholder for Name input
315 | */
316 | public function getPlaceholderForName()
317 | {
318 | return Yii::t('voskobovich/nestedsets', 'Node name');
319 | }
320 |
321 | /**
322 | * Кнопки действий над виджетом
323 | */
324 | public function actionButtons()
325 | {
326 | echo Html::beginTag('div', ['class' => "{$this->id}-nestable-menu"]);
327 |
328 | echo Html::beginTag('div', ['class' => 'btn-group']);
329 | echo Html::button(Yii::t('voskobovich/nestedsets', 'Add node'), [
330 | 'data-toggle' => 'modal',
331 | 'data-target' => "#{$this->id}-new-node-modal",
332 | 'class' => 'btn btn-success'
333 | ]);
334 | echo Html::button(Yii::t('voskobovich/nestedsets', 'Collapse all'), [
335 | 'data-action' => 'collapse-all',
336 | 'class' => 'btn btn-default'
337 | ]);
338 | echo Html::button(Yii::t('voskobovich/nestedsets', 'Expand all'), [
339 | 'data-action' => 'expand-all',
340 | 'class' => 'btn btn-default',
341 | 'style' => 'display: none'
342 | ]);
343 | echo Html::endTag('div');
344 |
345 | echo Html::endTag('div');
346 | }
347 |
348 | /**
349 | * Вывод меню
350 | */
351 | private function renderMenu()
352 | {
353 | echo Html::beginTag('div', ['class' => 'dd-nestable', 'id' => $this->id]);
354 |
355 | $menu = (count($this->_items) > 0) ? $this->_items : [
356 | ['id' => 0, 'name' => $this->getPlaceholderForName()]
357 | ];
358 |
359 | $this->printLevel($menu);
360 |
361 | echo Html::endTag('div');
362 | }
363 |
364 | /**
365 | * Render form for new node
366 | */
367 | private function renderForm()
368 | {
369 | /** @var ActiveRecord $model */
370 | $model = new $this->modelClass;
371 |
372 | echo <<
374 |
375 |
376 | HTML;
377 | /** @var ActiveForm $form */
378 | $form = ActiveForm::begin([
379 | 'id' => $this->id . '-new-node-form'
380 | ]);
381 |
382 | echo <<
384 |
385 |
New node
386 |
387 |
388 | HTML;
389 |
390 | echo call_user_func($this->formFieldsCallable, $form, $model);
391 |
392 | echo <<
394 |
398 | HTML;
399 | $form->end();
400 | echo <<
402 |
403 |
404 | HTML;
405 | }
406 |
407 | /**
408 | * Распечатка одного уровня
409 | * @param $level
410 | */
411 | private function printLevel($level)
412 | {
413 | echo Html::beginTag('ol', ['class' => 'dd-list']);
414 |
415 | foreach ($level as $item) {
416 | $this->printItem($item);
417 | }
418 |
419 | echo Html::endTag('ol');
420 | }
421 |
422 | /**
423 | * Распечатка одного пункта
424 | * @param $item
425 | */
426 | private function printItem($item)
427 | {
428 | $htmlOptions = ['class' => 'dd-item'];
429 | $htmlOptions['data-id'] = !empty($item['id']) ? $item['id'] : '';
430 |
431 | echo Html::beginTag('li', $htmlOptions);
432 |
433 | echo Html::tag('div', '', ['class' => 'dd-handle']);
434 | echo Html::tag('div', $item['name'], ['class' => 'dd-content']);
435 |
436 | echo Html::beginTag('div', ['class' => 'dd-edit-panel']);
437 | echo Html::input('text', null, $item['name'],
438 | ['class' => 'dd-input-name', 'placeholder' => $this->getPlaceholderForName()]);
439 |
440 | echo Html::beginTag('div', ['class' => 'btn-group']);
441 | echo Html::button(Yii::t('voskobovich/nestedsets', 'Save'), [
442 | 'data-action' => 'save',
443 | 'class' => 'btn btn-success btn-sm',
444 | ]);
445 | echo Html::a(Yii::t('voskobovich/nestedsets', 'Advanced editing'), $item['update-url'], [
446 | 'data-action' => 'advanced-editing',
447 | 'class' => 'btn btn-default btn-sm',
448 | 'target' => '_blank'
449 | ]);
450 | echo Html::button(Yii::t('voskobovich/nestedsets', 'Delete'), [
451 | 'data-action' => 'delete',
452 | 'class' => 'btn btn-danger btn-sm'
453 | ]);
454 | echo Html::endTag('div');
455 |
456 | echo Html::endTag('div');
457 |
458 | if (isset($item['children']) && count($item['children'])) {
459 | $this->printLevel($item['children']);
460 | }
461 |
462 | echo Html::endTag('li');
463 | }
464 | }
--------------------------------------------------------------------------------
/src/widgets/nestable/NestableAsset.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/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 id = el.data('id');
340 | if (typeof id === "undefined" || !id) {
341 | return false;
342 | }
343 |
344 | var prev = el.prev(this.options.itemNodeName);
345 | var next = el.next(this.options.itemNodeName);
346 | var parent = el.parents(this.options.itemNodeName);
347 |
348 | $.ajax({
349 | url: this.options.moveUrl + '?id=' + id,
350 | method: 'POST',
351 | context: document.body,
352 | data: {
353 | parent: $(parent).data('id'),
354 | left: (prev.length ? prev.data('id') : 0),
355 | right: (next.length ? next.data('id') : 0)
356 | }
357 | }).fail(function (jqXHR) {
358 | alert(jqXHR.responseText);
359 | });
360 | },
361 |
362 | deleteNodeRequest: function (el) {
363 | var id = el.data('id');
364 | if (typeof id === "undefined" || !id) {
365 | return false;
366 | }
367 |
368 | $.ajax({
369 | url: this.options.deleteUrl + '?id=' + id,
370 | method: 'POST',
371 | context: document.body,
372 | }).success(function (data, textStatus, jqXHR) {
373 | el.remove();
374 | }).fail(function (jqXHR) {
375 | alert(jqXHR.responseText);
376 | });
377 | },
378 |
379 | updateNodeRequest: function (el) {
380 | var tree = this,
381 | id = el.data('id');
382 |
383 | if (typeof id === "undefined" || !id) {
384 | return false;
385 | }
386 |
387 | var name = el.find('.' + this.options.inputNameClass);
388 |
389 | $.ajax({
390 | url: this.options.updateUrl + '?id=' + id,
391 | method: 'POST',
392 | context: document.body,
393 | data: {
394 | name: name.val()
395 | }
396 | }).success(function (data, textStatus, jqXHR) {
397 | var editPanel = el.children('.' + tree.options.editPanelClass);
398 | editPanel.removeClass(tree.options.inputOpenClass);
399 | editPanel.slideUp(100);
400 | }).fail(function (jqXHR) {
401 | alert(jqXHR.responseText);
402 | });
403 | },
404 |
405 | dragMove: function (e) {
406 | var tree, parent, prev, next, depth,
407 | opt = this.options,
408 | mouse = this.mouse;
409 |
410 | this.dragEl.css({
411 | 'left': e.pageX - mouse.offsetX,
412 | 'top': e.pageY - mouse.offsetY
413 | });
414 |
415 | // mouse position last events
416 | mouse.lastX = mouse.nowX;
417 | mouse.lastY = mouse.nowY;
418 | // mouse position this events
419 | mouse.nowX = e.pageX;
420 | mouse.nowY = e.pageY;
421 | // distance mouse moved between events
422 | mouse.distX = mouse.nowX - mouse.lastX;
423 | mouse.distY = mouse.nowY - mouse.lastY;
424 | // direction mouse was moving
425 | mouse.lastDirX = mouse.dirX;
426 | mouse.lastDirY = mouse.dirY;
427 | // direction mouse is now moving (on both axis)
428 | mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
429 | mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
430 | // axis mouse is now moving on
431 | var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
432 |
433 | // do nothing on first move
434 | if (!mouse.moving) {
435 | mouse.dirAx = newAx;
436 | mouse.moving = true;
437 | return;
438 | }
439 |
440 | // calc distance moved on this axis (and direction)
441 | if (mouse.dirAx !== newAx) {
442 | mouse.distAxX = 0;
443 | mouse.distAxY = 0;
444 | } else {
445 | mouse.distAxX += Math.abs(mouse.distX);
446 | if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
447 | mouse.distAxX = 0;
448 | }
449 | mouse.distAxY += Math.abs(mouse.distY);
450 | if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
451 | mouse.distAxY = 0;
452 | }
453 | }
454 | mouse.dirAx = newAx;
455 |
456 | /**
457 | * move horizontal
458 | */
459 | if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
460 | // reset move distance on x-axis for new phase
461 | mouse.distAxX = 0;
462 | prev = this.placeEl.prev(opt.itemNodeName);
463 | // increase horizontal level if previous sibling exists and is not collapsed
464 | if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) {
465 | // cannot increase level when item above is collapsed
466 | tree = prev.find(opt.listNodeName).last();
467 | // check if depth limit has reached
468 | depth = this.placeEl.parents(opt.listNodeName).length;
469 | if (depth + this.dragDepth <= opt.maxDepth) {
470 | // create new sub-level if one doesn't exist
471 | if (!tree.length) {
472 | tree = $('<' + opt.listNodeName + '/>').addClass(opt.listClass);
473 | tree.append(this.placeEl);
474 | prev.append(tree);
475 | this.setParent(prev);
476 | } else {
477 | // else append to next level up
478 | tree = prev.children(opt.listNodeName).last();
479 | tree.append(this.placeEl);
480 | }
481 | }
482 | }
483 | // decrease horizontal level
484 | if (mouse.distX < 0) {
485 | // we can't decrease a level if an item preceeds the current one
486 | next = this.placeEl.next(opt.itemNodeName);
487 | if (!next.length) {
488 | parent = this.placeEl.parent();
489 | this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
490 | if (!parent.children().length) {
491 | this.unsetParent(parent.parent());
492 | }
493 | }
494 | }
495 | }
496 |
497 | var isEmpty = false;
498 |
499 | // find list item under cursor
500 | if (!hasPointerEvents) {
501 | this.dragEl[0].style.visibility = 'hidden';
502 | }
503 | this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
504 | if (!hasPointerEvents) {
505 | this.dragEl[0].style.visibility = 'visible';
506 | }
507 | if (this.pointEl.hasClass(opt.handleClass)) {
508 | this.pointEl = this.pointEl.parent(opt.itemNodeName);
509 | }
510 | if (this.pointEl.hasClass(opt.emptyClass)) {
511 | isEmpty = true;
512 | }
513 | else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
514 | return;
515 | }
516 |
517 | // find parent list of item under cursor
518 | var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
519 | isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
520 |
521 | /**
522 | * move vertical
523 | */
524 | if (!mouse.dirAx || isNewRoot || isEmpty) {
525 | // check if groups match if dragging over new root
526 | if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
527 | return;
528 | }
529 | // check depth limit
530 | depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
531 | if (depth > opt.maxDepth) {
532 | return;
533 | }
534 | var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
535 | parent = this.placeEl.parent();
536 | // if empty create new list to replace empty placeholder
537 | if (isEmpty) {
538 | tree = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
539 | tree.append(this.placeEl);
540 | this.pointEl.replaceWith(tree);
541 | }
542 | else if (before) {
543 | this.pointEl.before(this.placeEl);
544 | }
545 | else {
546 | this.pointEl.after(this.placeEl);
547 | }
548 | if (!parent.children().length) {
549 | this.unsetParent(parent.parent());
550 | }
551 | if (!this.dragRootEl.find(opt.itemNodeName).length) {
552 | this.dragRootEl.append('');
553 | }
554 | // parent root list has changed
555 | if (isNewRoot) {
556 | this.dragRootEl = pointElRoot;
557 | this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
558 | }
559 | }
560 | }
561 |
562 | };
563 |
564 | $.fn.nestable = function (params) {
565 | var lists = this,
566 | retval = this;
567 |
568 | lists.each(function () {
569 | var plugin = $(this).data("nestable");
570 |
571 | if (!plugin) {
572 | $(this).data("nestable", new Plugin(this, params));
573 | $(this).data("nestable-id", new Date().getTime());
574 | } else {
575 | if (typeof params === 'string' && typeof plugin[params] === 'function') {
576 | retval = plugin[params]();
577 | }
578 | }
579 | });
580 |
581 | return retval || lists;
582 | };
583 |
584 | })(window.jQuery || window.Zepto, window, document);
585 |
--------------------------------------------------------------------------------