├── .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 | '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 | 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 | --------------------------------------------------------------------------------