├── nestable ├── NestableAsset.php ├── NestableBehavior.php ├── NodeMoveAction.php └── Nestable.php ├── composer.json ├── LICENSE ├── README.md └── assets ├── css ├── nestable.min.css └── nestable.css └── js ├── jquery.nestable.min.js └── jquery.nestable.js /nestable/NestableAsset.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 1.0 16 | */ 17 | class NestableAsset extends \kartik\base\AssetBundle { 18 | 19 | public function init() { 20 | $this->setSourcePath(__DIR__ . '/../assets'); 21 | $this->setupAssets('js', ['js/jquery.nestable']); 22 | $this->setupAssets('css', ['css/nestable']); 23 | parent::init(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slatiusa/yii2-nestable", 3 | "description": "Yii 2.0 implementation of nested set behavior using jquery.nestable plugin.", 4 | "keywords": ["yii2", "extension", "widget", "nestable", "sortable", "nested set", "behavior", "jquery"], 5 | "homepage": "https://github.com/Hommer101/yii2-nestable", 6 | "type": "yii2-extension", 7 | "license": "BSD 3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Arno Slatius", 11 | "email": "a.slatius@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "kartik-v/yii2-krajee-base": "*", 16 | "creocoder/yii2-nested-sets": "*" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "slatiusa\\": "" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nestable/NestableBehavior.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 1.0 19 | */ 20 | class NestableBehavior extends NestedSetsBehavior 21 | { 22 | /** 23 | * Wrapper function to be able to use the protected method of the NestedSetsBehavior 24 | * 25 | * @param integer $value 26 | * @param integer $depth 27 | */ 28 | public function nodeMove($value, $depth) { 29 | $this->node = $this->owner; 30 | parent::moveNode($value, $depth); 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, iksnimak 2 | Copyright (c) 2015, slatiusa 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the {organization} nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | yii2-nestable 2 | ============= 3 | 4 | Yii 2.0 implementation of nested set behavior using jquery.nestable plugin. 5 | - jquery.nestable plugin: http://dbushell.github.io/Nestable/ 6 | - Nested Sets Behavior for Yii 2: https://github.com/creocoder/yii2-nested-sets 7 | 8 | ## Installation 9 | 10 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 11 | 12 | Either run 13 | 14 | ``` 15 | $ php composer.phar require slatiusa/yii2-nestable "dev-master" 16 | ``` 17 | 18 | or add 19 | 20 | ``` 21 | "slatiusa/yii2-nestable": "dev-master" 22 | ``` 23 | 24 | to the ```require``` section of your `composer.json` file. 25 | 26 | ## Usage 27 | 28 | Make sure you've attached the NestedSetsBehavior (creocoder/yii2-nested-sets) correctly to your model. 29 | Then add the node move handler to you controller by attaching the supplied action; 30 | 31 | ``` 32 | use slatiusa\nestable\Nestable; 33 | 34 | class yourClass extends Controller 35 | { 36 | public function actions() { 37 | return [ 38 | 'nodeMove' => [ 39 | 'class' => 'slatiusa\nestable\NodeMoveAction', 40 | 'modelName' => TreeModel::className(), 41 | ], 42 | ]; 43 | } 44 | 45 | ``` 46 | 47 | And then render the widget in your view; 48 | 49 | ``` 50 | echo Nestable::widget([ 51 | 'type' => Nestable::TYPE_WITH_HANDLE, 52 | 'query' => TreeModel::find()->where([ top of tree ]), 53 | 'modelOptions' => [ 54 | 'name' => 'name' 55 | ], 56 | 'pluginEvents' => [ 57 | 'change' => 'function(e) {}', 58 | ], 59 | 'pluginOptions' => [ 60 | 'maxDepth' => 7, 61 | ], 62 | ]); 63 | 64 | ``` 65 | 66 | You can either supply an ActiveQuery object in `query` from which a tree will be built. 67 | You can also supply an item list; 68 | ``` 69 | ... 70 | 'items' => [ 71 | ['content' => 'Item # 1', 'id' => 1], 72 | ['content' => 'Item # 2', 'id' => 2], 73 | ['content' => 'Item # 3', 'id' => 3], 74 | ['content' => 'Item # 4 with children', 'id' => 4, 'children' => [ 75 | ['content' => 'Item # 4.1', 'id' => 5], 76 | ['content' => 'Item # 4.2', 'id' => 6], 77 | ['content' => 'Item # 4.3', 'id' => 7], 78 | ]], 79 | ], 80 | ``` 81 | 82 | The `modelOptions['name']` should hold an attribute name that will be used to name on the items in the list. 83 | You can alternatively supply an unnamed `function($model)` to build your own content string. 84 | 85 | Supply a `pluginEvents['change']` with some JavaScript code to catch the change event fired by jquery.nestable plugin. 86 | The `pluginOptions` accepts all the options for the original jquery.nestable plugin. -------------------------------------------------------------------------------- /assets/css/nestable.min.css: -------------------------------------------------------------------------------- 1 | .dd,.dd-list{display:block;padding:0;list-style:none}.dd,.dd-empty,.dd-item,.dd-placeholder{margin:0;font-size:13px;line-height:20px;position:relative}.dd,.dd-item>button,.dd-list{position:relative}.dd-handle,.dd-item>button{font-weight:700;margin:5px 0}.dd-item>button,.dd3-handle{cursor:pointer;white-space:nowrap;overflow:hidden}.dd-list{margin:0}.dd-list .dd-list{padding-left:30px}.dd-collapsed .dd-list{display:none}.dd-empty,.dd-item,.dd-placeholder{display:block;padding:0;min-height:20px}.dd-handle{display:block;height:30px;padding:5px 10px;color:#333;text-decoration:none;border:1px solid #ccc;background:#fafafa;background:-webkit-linear-gradient(top,#fafafa 0,#eee 100%);background:-moz-linear-gradient(top,#fafafa 0,#eee 100%);background:linear-gradient(top,#fafafa 0,#eee 100%);-webkit-border-radius:3px;border-radius:3px;box-sizing:border-box;-moz-box-sizing:border-box}.dd-handle:hover{color:#2ea8e5;background:#fff}.dd-item>button{display:block;float:left;width:25px;height:20px;padding:0;text-indent:100%;border:0;background:0 0;font-size:12px;line-height:1;text-align:center}.dd-item>button:before{content:'+';display:block;position:absolute;width:100%;text-align:center;text-indent:0}.dd-item>button[data-action=collapse]:before{content:'-'}.dd-empty,.dd-placeholder{margin:5px 0;padding:0;min-height:30px;background:#f2fbff;border:1px dashed #b6bcbf;box-sizing:border-box;-moz-box-sizing:border-box}.dd-empty{border:1px dashed #bbb;min-height:100px;background-color:#e5e5e5;background-image:-webkit-linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff),-webkit-linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff);background-image:-moz-linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff),-moz-linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff);background-image:linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff),linear-gradient(45deg,#fff 25%,transparent 25%,transparent 75%,#fff 75%,#fff);background-size:60px 60px;background-position:0 0,30px 30px}.dd-dragel{position:absolute;pointer-events:none;z-index:9999}.dd-dragel>.dd-item .dd-handle{margin-top:0}.dd-dragel .dd-handle{-webkit-box-shadow:2px 4px 6px 0 rgba(0,0,0,.1);box-shadow:2px 4px 6px 0 rgba(0,0,0,.1)}.dd-hover>.dd-handle{background:#2ea8e5!important}.dd3-content{display:block;height:30px;margin:5px 0;padding:5px 10px 5px 40px;color:#333;text-decoration:none;font-weight:700;border:1px solid #ccc;background:#fafafa;background:-webkit-linear-gradient(top,#fafafa 0,#eee 100%);background:-moz-linear-gradient(top,#fafafa 0,#eee 100%);background:linear-gradient(top,#fafafa 0,#eee 100%);-webkit-border-radius:3px;border-radius:3px;box-sizing:border-box;-moz-box-sizing:border-box}.dd3-content:hover{color:#2ea8e5;background:#fff}.dd-dragel>.dd3-item>.dd3-content{margin:0}.dd3-item>button{margin-left:30px}.dd3-handle{position:absolute;margin:0;left:0;top:0;width:30px;text-indent:100%;border:1px solid #aaa;background:#ddd;background:-webkit-linear-gradient(top,#ddd 0,#bbb 100%);background:-moz-linear-gradient(top,#ddd 0,#bbb 100%);background:linear-gradient(top,#ddd 0,#bbb 100%);border-top-right-radius:0;border-bottom-right-radius:0}.dd3-handle:before{content:'≡';display:block;position:absolute;left:0;top:3px;width:100%;text-align:center;text-indent:0;color:#fff;font-size:20px;font-weight:400}.dd3-handle:hover{background:#ddd} -------------------------------------------------------------------------------- /nestable/NodeMoveAction.php: -------------------------------------------------------------------------------- 1 | 22 | * @since 1.0 23 | */ 24 | class NodeMoveAction extends Action 25 | { 26 | /** @var string class to use to locate the supplied data ids */ 27 | public $modelName; 28 | 29 | /** @var bool variable to support editing without possibility of creating a root elements */ 30 | public $rootable = true; 31 | 32 | /** @vars string the attribute names of the model that hold these attributes */ 33 | private $leftAttribute; 34 | private $rightAttribute; 35 | private $treeAttribute; 36 | private $depthAttribute; 37 | 38 | /** 39 | * Move a node (model) below the parent and in between left and right 40 | * 41 | * @param integer $id the primaryKey of the moved node 42 | * @param integer $lft the primaryKey of the node left of the moved node 43 | * @param integer $rgt the primaryKey of the node right to the moved node 44 | * @param integer $par the primaryKey of the parent of the moved node 45 | */ 46 | public function run($id=0, $lft=0, $rgt=0, $par=0) 47 | { 48 | if (null == $this->modelName) { 49 | throw new \yii\base\InvalidConfigException("No 'modelName' supplied on action initialization."); 50 | } 51 | 52 | /* response will be in JSON format */ 53 | Yii::$app->response->format = 'json'; 54 | 55 | /* Locate the supplied model, left, right and parent models */ 56 | $model = Yii::createObject(ActiveQuery::className(), [$this->modelName])->where(['id' => $id])->one(); 57 | $lft = Yii::createObject(ActiveQuery::className(), [$this->modelName])->where(['id' => $lft])->one(); 58 | $rgt = Yii::createObject(ActiveQuery::className(), [$this->modelName])->where(['id' => $rgt])->one(); 59 | $par = Yii::createObject(ActiveQuery::className(), [$this->modelName])->where(['id' => $par])->one(); 60 | 61 | /* Get attribute names from model behaviour config */ 62 | foreach($model->behaviors as $behavior) { 63 | if ($behavior instanceof NestedSetsBehavior) { 64 | $this->leftAttribute = $behavior->leftAttribute; 65 | $this->rightAttribute = $behavior->rightAttribute; 66 | $this->treeAttribute = $behavior->treeAttribute; 67 | $this->depthAttribute = $behavior->depthAttribute; 68 | break; 69 | } 70 | } 71 | 72 | /* attach our bahaviour to be able to call the moveNode() function of the NestedSetsBehavior */ 73 | $model->attachBehavior('nestable', [ 74 | 'class' => \slatiusa\nestable\NestableBehavior::className(), 75 | 'leftAttribute' => $this->leftAttribute, 76 | 'rightAttribute' => $this->rightAttribute, 77 | 'treeAttribute' => $this->treeAttribute, 78 | 'depthAttribute' => $this->depthAttribute, 79 | ]); 80 | 81 | /* Root/Append/Left/Right change */ 82 | if($this->rootable&&$this->treeAttribute&&is_null($par)&&!$model->isRoot()){ 83 | $model->makeRoot(); 84 | } else if(is_null($par)){ 85 | if(!is_null($rgt)) 86 | $model->insertBefore($rgt); 87 | else if(!is_null($lft)) 88 | $model->insertAfter($lft); 89 | }else{ 90 | if(!is_null($rgt)) 91 | $model->insertBefore($rgt); 92 | else if(!is_null($lft)) 93 | $model->insertAfter($lft); 94 | else 95 | $model->appendTo($par); 96 | } 97 | 98 | /* report new position */ 99 | return ['updated' => [ 100 | 'id' => $model->id, 101 | 'depth' => $model->{$this->depthAttribute}, 102 | 'lft' => $model->{$this->leftAttribute}, 103 | 'rgt' => $model->{$this->rightAttribute}, 104 | ]]; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /assets/css/nestable.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Nestable 3 | */ 4 | .dd { 5 | position: relative; 6 | display: block; 7 | margin: 0; 8 | padding: 0; 9 | list-style: none; 10 | font-size: 13px; 11 | line-height: 20px; 12 | } 13 | .dd-list { 14 | display: block; 15 | position: relative; 16 | margin: 0; 17 | padding: 0; 18 | list-style: none; 19 | } 20 | .dd-list .dd-list { 21 | padding-left: 30px; 22 | } 23 | .dd-collapsed .dd-list { 24 | display: none; 25 | } 26 | .dd-item, 27 | .dd-empty, 28 | .dd-placeholder { 29 | display: block; 30 | position: relative; 31 | margin: 0; 32 | padding: 0; 33 | min-height: 20px; 34 | font-size: 13px; 35 | line-height: 20px; 36 | } 37 | .dd-handle { 38 | display: block; 39 | height: 30px; 40 | margin: 5px 0; 41 | padding: 5px 10px; 42 | color: #333; 43 | text-decoration: none; 44 | font-weight: bold; 45 | border: 1px solid #ccc; 46 | background: #fafafa; 47 | background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); 48 | background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); 49 | background: linear-gradient(top, #fafafa 0%, #eee 100%); 50 | -webkit-border-radius: 3px; 51 | border-radius: 3px; 52 | box-sizing: border-box; 53 | -moz-box-sizing: border-box; 54 | } 55 | .dd-handle:hover { 56 | color: #2ea8e5; 57 | background: #fff; 58 | } 59 | .dd-item > button { 60 | display: block; 61 | position: relative; 62 | cursor: pointer; 63 | float: left; 64 | width: 25px; 65 | height: 20px; 66 | margin: 5px 0; 67 | padding: 0; 68 | text-indent: 100%; 69 | white-space: nowrap; 70 | overflow: hidden; 71 | border: 0; 72 | background: transparent; 73 | font-size: 12px; 74 | line-height: 1; 75 | text-align: center; 76 | font-weight: bold; 77 | } 78 | .dd-item > button:before { 79 | content: '+'; 80 | display: block; 81 | position: absolute; 82 | width: 100%; 83 | text-align: center; 84 | text-indent: 0; 85 | } 86 | .dd-item > button[data-action="collapse"]:before { 87 | content: '-'; 88 | } 89 | .dd-placeholder, 90 | .dd-empty { 91 | margin: 5px 0; 92 | padding: 0; 93 | min-height: 30px; 94 | background: #f2fbff; 95 | border: 1px dashed #b6bcbf; 96 | box-sizing: border-box; 97 | -moz-box-sizing: border-box; 98 | } 99 | .dd-empty { 100 | border: 1px dashed #bbb; 101 | min-height: 100px; 102 | background-color: #e5e5e5; 103 | background-image: -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); 104 | background-image: -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); 105 | background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); 106 | background-size: 60px 60px; 107 | background-position: 0 0, 30px 30px; 108 | } 109 | .dd-dragel { 110 | position: absolute; 111 | pointer-events: none; 112 | z-index: 9999; 113 | } 114 | .dd-dragel > .dd-item .dd-handle { 115 | margin-top: 0; 116 | } 117 | .dd-dragel .dd-handle { 118 | -webkit-box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, .1); 119 | box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, .1); 120 | } 121 | .dd-hover > .dd-handle { 122 | background: #2ea8e5 !important; 123 | } 124 | 125 | /** 126 | * Nestable Draggable Handles 127 | */ 128 | .dd3-content { 129 | display: block; 130 | height: 30px; 131 | margin: 5px 0; 132 | padding: 5px 10px 5px 40px; 133 | color: #333; 134 | text-decoration: none; 135 | font-weight: bold; 136 | border: 1px solid #ccc; 137 | background: #fafafa; 138 | background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); 139 | background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); 140 | background: linear-gradient(top, #fafafa 0%, #eee 100%); 141 | -webkit-border-radius: 3px; 142 | border-radius: 3px; 143 | box-sizing: border-box; 144 | -moz-box-sizing: border-box; 145 | } 146 | .dd3-content:hover { 147 | color: #2ea8e5; 148 | background: #fff; 149 | } 150 | .dd-dragel > .dd3-item > .dd3-content { 151 | margin: 0; 152 | } 153 | .dd3-item > button { 154 | margin-left: 30px; 155 | } 156 | .dd3-handle { 157 | position: absolute; 158 | margin: 0; 159 | left: 0; 160 | top: 0; 161 | cursor: pointer; 162 | width: 30px; 163 | text-indent: 100%; 164 | white-space: nowrap; 165 | overflow: hidden; 166 | border: 1px solid #aaa; 167 | background: #ddd; 168 | background: -webkit-linear-gradient(top, #ddd 0%, #bbb 100%); 169 | background: -moz-linear-gradient(top, #ddd 0%, #bbb 100%); 170 | background: linear-gradient(top, #ddd 0%, #bbb 100%); 171 | border-top-right-radius: 0; 172 | border-bottom-right-radius: 0; 173 | } 174 | .dd3-handle:before { 175 | content: '≡'; 176 | display: block; 177 | position: absolute; 178 | left: 0; 179 | top: 3px; 180 | width: 100%; 181 | text-align: center; 182 | text-indent: 0; 183 | color: #fff; 184 | font-size: 20px; 185 | font-weight: normal; 186 | } 187 | .dd3-handle:hover { 188 | background: #ddd; 189 | } -------------------------------------------------------------------------------- /nestable/Nestable.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 1.0 21 | */ 22 | class Nestable extends \kartik\base\Widget 23 | { 24 | const TYPE_LIST = 'list'; 25 | const TYPE_WITH_HANDLE = 'list-handle'; 26 | 27 | /** 28 | * @var string the type of the sortable widget 29 | * Defaults to Nestable::TYPE_WITH_HANDLE 30 | */ 31 | public $type = self::TYPE_WITH_HANDLE; 32 | 33 | /** 34 | * @var string, the handle label, this is not HTML encoded 35 | */ 36 | public $handleLabel = '
 
'; 37 | 38 | /** 39 | * @var array the HTML attributes to be applied to list. 40 | * This will be overridden by the [[options]] property within [[$items]]. 41 | */ 42 | public $listOptions = []; 43 | 44 | /** 45 | * @var array the HTML attributes to be applied to all items. 46 | * This will be overridden by the [[options]] property within [[$items]]. 47 | */ 48 | public $itemOptions = []; 49 | 50 | /** 51 | * @var array the sortable items configuration for rendering elements within the sortable 52 | * list / grid. You can set the following properties: 53 | * - id: integer, the id of the item. This will get returned on change 54 | * - content: string, the list item content (this is not HTML encoded) 55 | * - disabled: bool, whether the list item is disabled 56 | * - options: array, the HTML attributes for the list item. 57 | * - contentOptions: array, the HTML attributes for the content 58 | * - children: array, with item children 59 | */ 60 | public $items = []; 61 | 62 | /** 63 | * @var string the URL to send the callback to. Defaults to current controller / actionNodeMove which 64 | * can be provided by \slatiusa\nestable\nestableNodeMoveAction by registering that as an action in the 65 | * controller rendering the Widget. 66 | * ``` 67 | * public function actions() { 68 | * return [ 69 | * 'nodeMove' => [ 70 | * 'class' => 'slatiusa\nestable\NestableNodeMoveAction', 71 | * ], 72 | * ]; 73 | * } 74 | * ``` 75 | * Defaults to [current controller/nodeMove] if not set. 76 | */ 77 | public $url; 78 | 79 | /** 80 | * @var ActiveQuery that holds the data for the tree to show. 81 | */ 82 | public $query; 83 | 84 | /** 85 | * @var array options to be used with the model on list preparation. Supporten properties: 86 | * - name: {string|function}, attribute name for the item title or unnamed function($model) that returns a 87 | * string for each item. 88 | */ 89 | public $modelOptions = []; 90 | 91 | /** 92 | * Initializes the widget 93 | */ 94 | public function init() { 95 | if (null != $this->url) { 96 | $this->pluginOptions['url'] = $this->url; 97 | } else { 98 | $this->pluginOptions['url'] = Url::to([$this->view->context->id.'/nodeMove']); 99 | } 100 | 101 | parent::init(); 102 | $this->registerAssets(); 103 | 104 | Html::addCssClass($this->options, 'dd'); 105 | echo Html::beginTag('div', $this->options); 106 | 107 | if (null != $this->query) { 108 | $this->items = $this->prepareItems($this->query); 109 | } 110 | if (count($this->items) === 0) { 111 | echo Html::tag('div', '', ['class' => 'dd-empty']); 112 | } 113 | } 114 | 115 | /** 116 | * Runs the widget 117 | * 118 | * @return string|void 119 | */ 120 | public function run() { 121 | if (count($this->items) > 0) { 122 | echo Html::beginTag('ol', ['class' => 'dd-list']); 123 | echo $this->renderItems(); 124 | echo Html::endTag('ol'); 125 | } 126 | echo Html::endTag('div'); 127 | } 128 | 129 | /** 130 | * Render the list items for the sortable widget 131 | * 132 | * @return string 133 | */ 134 | protected function renderItems($_items = NULL) { 135 | $_items = is_null($_items) ? $this->items : $_items; 136 | $items = ''; 137 | $dataid = 0; 138 | foreach ($_items as $item) { 139 | $options = ArrayHelper::getValue($item, 'options', ['class' => 'dd-item dd3-item']); 140 | $options = ArrayHelper::merge($this->itemOptions, $options); 141 | $dataId = ArrayHelper::getValue($item, 'id', $dataid++); 142 | $options = ArrayHelper::merge($options, ['data-id' => $dataId]); 143 | 144 | $contentOptions = ArrayHelper::getValue($item, 'contentOptions', ['class' => 'dd3-content']); 145 | $content = $this->handleLabel; 146 | $content .= Html::tag('div', ArrayHelper::getValue($item, 'content', ''), $contentOptions); 147 | 148 | $children = ArrayHelper::getValue($item, 'children', []); 149 | if (!empty($children)) { 150 | // recursive rendering children items 151 | $content .= Html::beginTag('ol', ['class' => 'dd-list']); 152 | $content .= $this->renderItems($children); 153 | $content .= Html::endTag('ol'); 154 | } 155 | 156 | $items .= Html::tag('li', $content, $options) . PHP_EOL; 157 | } 158 | return $items; 159 | } 160 | 161 | /** 162 | * Register client assets 163 | */ 164 | public function registerAssets() { 165 | $view = $this->getView(); 166 | NestableAsset::register($view); 167 | $this->registerPlugin('nestable'); 168 | } 169 | 170 | /** 171 | * @param $partial 172 | * @param $arguments 173 | */ 174 | public function renderContent($partial, $arguments) { 175 | return $this->render($partial, $arguments); 176 | } 177 | 178 | /** 179 | * put your comment there... 180 | * 181 | * @param $activeQuery \yii\db\ActiveQuery 182 | * @return array 183 | */ 184 | protected function prepareItems($activeQuery) 185 | { 186 | $items = []; 187 | foreach ($activeQuery->all() as $model) { 188 | $name = ArrayHelper::getValue($this->modelOptions, 'name', 'name'); 189 | $items[] = [ 190 | 'id' => $model->getPrimaryKey(), 191 | 'content' => (is_callable($name) ? call_user_func($name, $model) : $model->{$name}), 192 | 'children' => $this->prepareItems($model->children(1)), 193 | ]; 194 | } 195 | return $items; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /assets/js/jquery.nestable.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/ 3 | * Modified for yii2-nestable - Copyright (c) 2015 Arno Slatius - http://slatius.nl/ 4 | * 5 | * Dual-licensed under the BSD or MIT licenses 6 | */ 7 | !function(t,e,s,i){function a(s,i){this.w=t(e),this.el=t(s),this.options=t.extend({},h,i),this.init()}var o="ontouchstart"in e,n=function(){var t=s.createElement("div"),i=s.documentElement;if(!("pointerEvents"in t.style))return!1;t.style.pointerEvents="auto",t.style.pointerEvents="x",i.appendChild(t);var a=e.getComputedStyle&&"auto"===e.getComputedStyle(t,"").pointerEvents;return i.removeChild(t),!!a}(),l=o?"touchstart":"mousedown",d=o?"touchmove":"mousemove",r=o?"touchend":"mouseup";eCancel=o?"touchcancel":"mouseup";var h={listNodeName:"ol",itemNodeName:"li",rootClass:"dd",listClass:"dd-list",itemClass:"dd-item",dragClass:"dd-dragel",handleClass:"dd-handle",collapsedClass:"dd-collapsed",placeClass:"dd-placeholder",noDragClass:"dd-nodrag",emptyClass:"dd-empty",expandBtnHTML:'',collapseBtnHTML:'',group:0,maxDepth:5,threshold:20,url:""};a.prototype={init:function(){var s=this;s.reset(),s.el.data("nestable-group",this.options.group),s.placeEl=t('
'),t.each(this.el.find(s.options.itemNodeName),function(e,i){s.setParent(t(i))}),s.el.on("click","button",function(e){if(!s.dragEl&&(o||0===e.button)){var i=t(e.currentTarget),a=i.data("action"),n=i.parent(s.options.itemNodeName);"collapse"===a&&s.collapseItem(n),"expand"===a&&s.expandItem(n)}});var i=function(e){var i=t(e.target);if(!i.hasClass(s.options.handleClass)){if(i.closest("."+s.options.noDragClass).length)return;i=i.closest("."+s.options.handleClass)}!i.length||s.dragEl||!o&&0!==e.button||o&&1!==e.touches.length||(e.preventDefault(),s.dragStart(o?e.touches[0]:e))},a=function(t){s.dragEl&&(t.preventDefault(),s.dragMove(o?t.touches[0]:t))},n=function(t){s.dragEl&&(t.preventDefault(),s.dragStop(o?t.touches[0]:t))};o?(s.el[0].addEventListener(l,i,!1),e.addEventListener(d,a,!1),e.addEventListener(r,n,!1),e.addEventListener(eCancel,n,!1)):(s.el.on(l,i),s.w.on(d,a),s.w.on(r,n))},serialize:function(){var e,s=0,i=this;return step=function(e,s){var a=[],o=e.children(i.options.itemNodeName);return o.each(function(){var e=t(this),o=t.extend({},e.data()),n=e.children(i.options.listNodeName);n.length&&(o.children=step(n,s+1)),a.push(o)}),a},e=step(i.el.find(i.options.listNodeName).first(),s)},serialise:function(){return this.serialize()},reset:function(){this.mouse={offsetX:0,offsetY:0,startX:0,startY:0,lastX:0,lastY:0,nowX:0,nowY:0,distX:0,distY:0,dirAx:0,dirX:0,dirY:0,lastDirX:0,lastDirY:0,distAxX:0,distAxY:0},this.moving=!1,this.dragEl=null,this.dragRootEl=null,this.dragDepth=0,this.hasNewRoot=!1,this.pointEl=null},expandItem:function(t){t.removeClass(this.options.collapsedClass),t.children('[data-action="expand"]').hide(),t.children('[data-action="collapse"]').show(),t.children(this.options.listNodeName).show()},collapseItem:function(t){var e=t.children(this.options.listNodeName);e.length&&(t.addClass(this.options.collapsedClass),t.children('[data-action="collapse"]').hide(),t.children('[data-action="expand"]').show(),t.children(this.options.listNodeName).hide())},expandAll:function(){var e=this;e.el.find(e.options.itemNodeName).each(function(){e.expandItem(t(this))})},collapseAll:function(){var e=this;e.el.find(e.options.itemNodeName).each(function(){e.collapseItem(t(this))})},setParent:function(e){e.children(this.options.listNodeName).length&&(e.prepend(t(this.options.expandBtnHTML)),e.prepend(t(this.options.collapseBtnHTML))),e.children('[data-action="expand"]').hide()},unsetParent:function(t){t.removeClass(this.options.collapsedClass),t.children("[data-action]").remove(),t.children(this.options.listNodeName).remove()},dragStart:function(e){{var a=this.mouse,o=t(e.target),n=o.closest(this.options.itemNodeName);this.node}this.placeEl.css("height",n.height()),a.offsetX=e.offsetX!==i?e.offsetX:e.pageX-o.offset().left,a.offsetY=e.offsetY!==i?e.offsetY:e.pageY-o.offset().top,a.startX=a.lastX=e.pageX,a.startY=a.lastY=e.pageY,this.dragRootEl=this.el,this.dragEl=t(s.createElement(this.options.listNodeName)).addClass(this.options.listClass+" "+this.options.dragClass),this.dragEl.css("width",n.width()),n.after(this.placeEl),n[0].parentNode.removeChild(n[0]),n.appendTo(this.dragEl),t(s.body).append(this.dragEl),this.dragEl.css({left:e.pageX-a.offsetX,top:e.pageY-a.offsetY});var l,d,r=this.dragEl.find(this.options.itemNodeName);for(l=0;lthis.dragDepth&&(this.dragDepth=d)},dragStop:function(){var e=this.dragEl.children(this.options.itemNodeName).first();e[0].parentNode.removeChild(e[0]),this.placeEl.replaceWith(e),this.dragEl.remove();var i=e.prev(this.options.itemNodeName),a=e.next(this.options.itemNodeName),o=e.parents(this.options.itemNodeName);t.ajax({url:this.options.url,context:s.body,data:{id:e.data("id"),par:t(o).data("id"),lft:i.length?i.data("id"):0,rgt:a.length?a.data("id"):0}}).fail(function(jqXHR){alert(jqXHR.responseText);}),this.el.trigger("change"),this.hasNewRoot&&this.dragRootEl.trigger("change"),this.reset()},dragMove:function(i){var a,o,l,d,r,h=this.options,p=this.mouse;this.dragEl.css({left:i.pageX-p.offsetX,top:i.pageY-p.offsetY}),p.lastX=p.nowX,p.lastY=p.nowY,p.nowX=i.pageX,p.nowY=i.pageY,p.distX=p.nowX-p.lastX,p.distY=p.nowY-p.lastY,p.lastDirX=p.dirX,p.lastDirY=p.dirY,p.dirX=0===p.distX?0:p.distX>0?1:-1,p.dirY=0===p.distY?0:p.distY>0?1:-1;var c=Math.abs(p.distX)>Math.abs(p.distY)?1:0;if(!p.moving)return p.dirAx=c,void(p.moving=!0);p.dirAx!==c?(p.distAxX=0,p.distAxY=0):(p.distAxX+=Math.abs(p.distX),0!==p.dirX&&p.dirX!==p.lastDirX&&(p.distAxX=0),p.distAxY+=Math.abs(p.distY),0!==p.dirY&&p.dirY!==p.lastDirY&&(p.distAxY=0)),p.dirAx=c,p.dirAx&&p.distAxX>=h.threshold&&(p.distAxX=0,l=this.placeEl.prev(h.itemNodeName),p.distX>0&&l.length&&!l.hasClass(h.collapsedClass)&&(a=l.find(h.listNodeName).last(),r=this.placeEl.parents(h.listNodeName).length,r+this.dragDepth<=h.maxDepth&&(a.length?(a=l.children(h.listNodeName).last(),a.append(this.placeEl)):(a=t("<"+h.listNodeName+"/>").addClass(h.listClass),a.append(this.placeEl),l.append(a),this.setParent(l)))),p.distX<0&&(d=this.placeEl.next(h.itemNodeName),d.length||(o=this.placeEl.parent(),this.placeEl.closest(h.itemNodeName).after(this.placeEl),o.children().length||this.unsetParent(o.parent()))));var g=!1;if(n||(this.dragEl[0].style.visibility="hidden"),this.pointEl=t(s.elementFromPoint(i.pageX-s.body.scrollLeft,i.pageY-(e.pageYOffset||s.documentElement.scrollTop))),n||(this.dragEl[0].style.visibility="visible"),this.pointEl.hasClass(h.handleClass)&&(this.pointEl=this.pointEl.parent(h.itemNodeName)),this.pointEl.hasClass(h.emptyClass))g=!0;else if(!this.pointEl.length||!this.pointEl.hasClass(h.itemClass))return;var m=this.pointEl.closest("."+h.rootClass),f=this.dragRootEl.data("nestable-id")!==m.data("nestable-id");if(!p.dirAx||f||g){if(f&&h.group!==m.data("nestable-group"))return;if(r=this.dragDepth-1+this.pointEl.parents(h.listNodeName).length,r>h.maxDepth)return;var u=i.pageY'),f&&(this.dragRootEl=m,this.hasNewRoot=this.el[0]!==this.dragRootEl[0])}}},t.fn.nestable=function(e){var s=this,i=this;return s.each(function(){var s=t(this).data("nestable");s?"string"==typeof e&&"function"==typeof s[e]&&(i=s[e]()):(t(this).data("nestable",new a(this,e)),t(this).data("nestable-id",(new Date).getTime()))}),i||s}}(window.jQuery||window.Zepto,window,document); -------------------------------------------------------------------------------- /assets/js/jquery.nestable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/ 3 | * Modified for yii2-nestable - Copyright (c) 2015 Arno Slatius - http://slatius.nl/ 4 | * 5 | * Dual-licensed under the BSD or MIT licenses 6 | */ 7 | ;(function($, window, document, undefined) 8 | { 9 | var hasTouch = 'ontouchstart' in window; 10 | 11 | /** 12 | * Detect CSS pointer-events property 13 | * events are normally disabled on the dragging element to avoid conflicts 14 | * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js 15 | */ 16 | var hasPointerEvents = (function() 17 | { 18 | var el = document.createElement('div'), 19 | docEl = document.documentElement; 20 | if (!('pointerEvents' in el.style)) { 21 | return false; 22 | } 23 | el.style.pointerEvents = 'auto'; 24 | el.style.pointerEvents = 'x'; 25 | docEl.appendChild(el); 26 | var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto'; 27 | docEl.removeChild(el); 28 | return !!supports; 29 | })(); 30 | 31 | var eStart = hasTouch ? 'touchstart' : 'mousedown', 32 | eMove = hasTouch ? 'touchmove' : 'mousemove', 33 | eEnd = hasTouch ? 'touchend' : 'mouseup'; 34 | eCancel = hasTouch ? 'touchcancel' : 'mouseup'; 35 | 36 | var defaults = { 37 | listNodeName : 'ol', 38 | itemNodeName : 'li', 39 | rootClass : 'dd', 40 | listClass : 'dd-list', 41 | itemClass : 'dd-item', 42 | dragClass : 'dd-dragel', 43 | handleClass : 'dd-handle', 44 | collapsedClass : 'dd-collapsed', 45 | placeClass : 'dd-placeholder', 46 | noDragClass : 'dd-nodrag', 47 | emptyClass : 'dd-empty', 48 | expandBtnHTML : '', 49 | collapseBtnHTML : '', 50 | group : 0, 51 | maxDepth : 5, 52 | threshold : 20, 53 | url : '' 54 | }; 55 | 56 | function Plugin(element, options) 57 | { 58 | this.w = $(window); 59 | this.el = $(element); 60 | this.options = $.extend({}, defaults, options); 61 | this.init(); 62 | } 63 | 64 | Plugin.prototype = { 65 | 66 | init: function() 67 | { 68 | var list = this; 69 | 70 | list.reset(); 71 | 72 | list.el.data('nestable-group', this.options.group); 73 | 74 | list.placeEl = $('
'); 75 | 76 | $.each(this.el.find(list.options.itemNodeName), function(k, el) { 77 | list.setParent($(el)); 78 | }); 79 | 80 | list.el.on('click', 'button', function(e) { 81 | if (list.dragEl || (!hasTouch && e.button !== 0)) { 82 | return; 83 | } 84 | var target = $(e.currentTarget), 85 | action = target.data('action'), 86 | item = target.parent(list.options.itemNodeName); 87 | if (action === 'collapse') { 88 | list.collapseItem(item); 89 | } 90 | if (action === 'expand') { 91 | list.expandItem(item); 92 | } 93 | }); 94 | 95 | var onStartEvent = function(e) 96 | { 97 | var handle = $(e.target); 98 | if (!handle.hasClass(list.options.handleClass)) { 99 | if (handle.closest('.' + list.options.noDragClass).length) { 100 | return; 101 | } 102 | handle = handle.closest('.' + list.options.handleClass); 103 | } 104 | if (!handle.length || list.dragEl || (!hasTouch && e.button !== 0) || (hasTouch && e.touches.length !== 1)) { 105 | return; 106 | } 107 | e.preventDefault(); 108 | list.dragStart(hasTouch ? e.touches[0] : e); 109 | }; 110 | 111 | var onMoveEvent = function(e) 112 | { 113 | if (list.dragEl) { 114 | e.preventDefault(); 115 | list.dragMove(hasTouch ? e.touches[0] : e); 116 | } 117 | }; 118 | 119 | var onEndEvent = function(e) 120 | { 121 | if (list.dragEl) { 122 | e.preventDefault(); 123 | list.dragStop(hasTouch ? e.touches[0] : e); 124 | } 125 | }; 126 | 127 | if (hasTouch) { 128 | list.el[0].addEventListener(eStart, onStartEvent, false); 129 | window.addEventListener(eMove, onMoveEvent, false); 130 | window.addEventListener(eEnd, onEndEvent, false); 131 | window.addEventListener(eCancel, onEndEvent, false); 132 | } else { 133 | list.el.on(eStart, onStartEvent); 134 | list.w.on(eMove, onMoveEvent); 135 | list.w.on(eEnd, onEndEvent); 136 | } 137 | 138 | }, 139 | 140 | serialize: function() 141 | { 142 | var data, 143 | depth = 0, 144 | list = this; 145 | step = function(level, depth) 146 | { 147 | var array = [ ], 148 | items = level.children(list.options.itemNodeName); 149 | items.each(function() 150 | { 151 | var li = $(this), 152 | item = $.extend({}, li.data()), 153 | sub = li.children(list.options.listNodeName); 154 | if (sub.length) { 155 | item.children = step(sub, depth + 1); 156 | } 157 | array.push(item); 158 | }); 159 | return array; 160 | }; 161 | data = step(list.el.find(list.options.listNodeName).first(), depth); 162 | return data; 163 | }, 164 | 165 | serialise: function() 166 | { 167 | return this.serialize(); 168 | }, 169 | 170 | reset: function() 171 | { 172 | this.mouse = { 173 | offsetX : 0, 174 | offsetY : 0, 175 | startX : 0, 176 | startY : 0, 177 | lastX : 0, 178 | lastY : 0, 179 | nowX : 0, 180 | nowY : 0, 181 | distX : 0, 182 | distY : 0, 183 | dirAx : 0, 184 | dirX : 0, 185 | dirY : 0, 186 | lastDirX : 0, 187 | lastDirY : 0, 188 | distAxX : 0, 189 | distAxY : 0 190 | }; 191 | this.moving = false; 192 | this.dragEl = null; 193 | this.dragRootEl = null; 194 | this.dragDepth = 0; 195 | this.hasNewRoot = false; 196 | this.pointEl = null; 197 | }, 198 | 199 | expandItem: function(li) 200 | { 201 | li.removeClass(this.options.collapsedClass); 202 | li.children('[data-action="expand"]').hide(); 203 | li.children('[data-action="collapse"]').show(); 204 | li.children(this.options.listNodeName).show(); 205 | }, 206 | 207 | collapseItem: function(li) 208 | { 209 | var lists = li.children(this.options.listNodeName); 210 | if (lists.length) { 211 | li.addClass(this.options.collapsedClass); 212 | li.children('[data-action="collapse"]').hide(); 213 | li.children('[data-action="expand"]').show(); 214 | li.children(this.options.listNodeName).hide(); 215 | } 216 | }, 217 | 218 | expandAll: function() 219 | { 220 | var list = this; 221 | list.el.find(list.options.itemNodeName).each(function() { 222 | list.expandItem($(this)); 223 | }); 224 | }, 225 | 226 | collapseAll: function() 227 | { 228 | var list = this; 229 | list.el.find(list.options.itemNodeName).each(function() { 230 | list.collapseItem($(this)); 231 | }); 232 | }, 233 | 234 | setParent: function(li) 235 | { 236 | if (li.children(this.options.listNodeName).length) { 237 | li.prepend($(this.options.expandBtnHTML)); 238 | li.prepend($(this.options.collapseBtnHTML)); 239 | } 240 | li.children('[data-action="expand"]').hide(); 241 | }, 242 | 243 | unsetParent: function(li) 244 | { 245 | li.removeClass(this.options.collapsedClass); 246 | li.children('[data-action]').remove(); 247 | li.children(this.options.listNodeName).remove(); 248 | }, 249 | 250 | dragStart: function(e) 251 | { 252 | var mouse = this.mouse, 253 | target = $(e.target), 254 | dragItem = target.closest(this.options.itemNodeName), 255 | node = this.node; 256 | 257 | this.placeEl.css('height', dragItem.height()); 258 | 259 | mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left; 260 | mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top; 261 | mouse.startX = mouse.lastX = e.pageX; 262 | mouse.startY = mouse.lastY = e.pageY; 263 | 264 | this.dragRootEl = this.el; 265 | 266 | this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass); 267 | this.dragEl.css('width', dragItem.width()); 268 | 269 | // fix for zepto.js 270 | //dragItem.after(this.placeEl).detach().appendTo(this.dragEl); 271 | dragItem.after(this.placeEl); 272 | dragItem[0].parentNode.removeChild(dragItem[0]); 273 | dragItem.appendTo(this.dragEl); 274 | 275 | $(document.body).append(this.dragEl); 276 | this.dragEl.css({ 277 | 'left' : e.pageX - mouse.offsetX, 278 | 'top' : e.pageY - mouse.offsetY 279 | }); 280 | // total depth of dragging item 281 | var i, depth, 282 | items = this.dragEl.find(this.options.itemNodeName); 283 | for (i = 0; i < items.length; i++) { 284 | depth = $(items[i]).parents(this.options.listNodeName).length; 285 | if (depth > this.dragDepth) { 286 | this.dragDepth = depth; 287 | } 288 | } 289 | }, 290 | 291 | dragStop: function(e) 292 | { 293 | var el = this.dragEl.children(this.options.itemNodeName).first(); 294 | el[0].parentNode.removeChild(el[0]); 295 | this.placeEl.replaceWith(el); 296 | 297 | this.dragEl.remove(); 298 | 299 | var prev = el.prev(this.options.itemNodeName); 300 | var next = el.next(this.options.itemNodeName); 301 | var parent = el.parents(this.options.itemNodeName); 302 | $.ajax({ 303 | url: this.options.url, 304 | context: document.body, 305 | data: { 306 | id : el.data('id'), 307 | par : $(parent).data('id'), 308 | lft : (prev.length ? prev.data('id') : 0), 309 | rgt : (next.length ? next.data('id') : 0) 310 | } 311 | }).fail(function(jqXHR){ 312 | alert(jqXHR.responseText); 313 | }); 314 | 315 | this.el.trigger('change'); 316 | if (this.hasNewRoot) { 317 | this.dragRootEl.trigger('change'); 318 | } 319 | this.reset(); 320 | }, 321 | 322 | dragMove: function(e) 323 | { 324 | var list, parent, prev, next, depth, 325 | opt = this.options, 326 | mouse = this.mouse; 327 | 328 | this.dragEl.css({ 329 | 'left' : e.pageX - mouse.offsetX, 330 | 'top' : e.pageY - mouse.offsetY 331 | }); 332 | 333 | // mouse position last events 334 | mouse.lastX = mouse.nowX; 335 | mouse.lastY = mouse.nowY; 336 | // mouse position this events 337 | mouse.nowX = e.pageX; 338 | mouse.nowY = e.pageY; 339 | // distance mouse moved between events 340 | mouse.distX = mouse.nowX - mouse.lastX; 341 | mouse.distY = mouse.nowY - mouse.lastY; 342 | // direction mouse was moving 343 | mouse.lastDirX = mouse.dirX; 344 | mouse.lastDirY = mouse.dirY; 345 | // direction mouse is now moving (on both axis) 346 | mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1; 347 | mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1; 348 | // axis mouse is now moving on 349 | var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0; 350 | 351 | // do nothing on first move 352 | if (!mouse.moving) { 353 | mouse.dirAx = newAx; 354 | mouse.moving = true; 355 | return; 356 | } 357 | 358 | // calc distance moved on this axis (and direction) 359 | if (mouse.dirAx !== newAx) { 360 | mouse.distAxX = 0; 361 | mouse.distAxY = 0; 362 | } else { 363 | mouse.distAxX += Math.abs(mouse.distX); 364 | if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) { 365 | mouse.distAxX = 0; 366 | } 367 | mouse.distAxY += Math.abs(mouse.distY); 368 | if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) { 369 | mouse.distAxY = 0; 370 | } 371 | } 372 | mouse.dirAx = newAx; 373 | 374 | /** 375 | * move horizontal 376 | */ 377 | if (mouse.dirAx && mouse.distAxX >= opt.threshold) { 378 | // reset move distance on x-axis for new phase 379 | mouse.distAxX = 0; 380 | prev = this.placeEl.prev(opt.itemNodeName); 381 | // increase horizontal level if previous sibling exists and is not collapsed 382 | if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) { 383 | // cannot increase level when item above is collapsed 384 | list = prev.find(opt.listNodeName).last(); 385 | // check if depth limit has reached 386 | depth = this.placeEl.parents(opt.listNodeName).length; 387 | if (depth + this.dragDepth <= opt.maxDepth) { 388 | // create new sub-level if one doesn't exist 389 | if (!list.length) { 390 | list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass); 391 | list.append(this.placeEl); 392 | prev.append(list); 393 | this.setParent(prev); 394 | } else { 395 | // else append to next level up 396 | list = prev.children(opt.listNodeName).last(); 397 | list.append(this.placeEl); 398 | } 399 | } 400 | } 401 | // decrease horizontal level 402 | if (mouse.distX < 0) { 403 | // we can't decrease a level if an item preceeds the current one 404 | next = this.placeEl.next(opt.itemNodeName); 405 | if (!next.length) { 406 | parent = this.placeEl.parent(); 407 | this.placeEl.closest(opt.itemNodeName).after(this.placeEl); 408 | if (!parent.children().length) { 409 | this.unsetParent(parent.parent()); 410 | } 411 | } 412 | } 413 | } 414 | 415 | var isEmpty = false; 416 | 417 | // find list item under cursor 418 | if (!hasPointerEvents) { 419 | this.dragEl[0].style.visibility = 'hidden'; 420 | } 421 | this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop))); 422 | if (!hasPointerEvents) { 423 | this.dragEl[0].style.visibility = 'visible'; 424 | } 425 | if (this.pointEl.hasClass(opt.handleClass)) { 426 | this.pointEl = this.pointEl.parent(opt.itemNodeName); 427 | } 428 | if (this.pointEl.hasClass(opt.emptyClass)) { 429 | isEmpty = true; 430 | } 431 | else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) { 432 | return; 433 | } 434 | 435 | // find parent list of item under cursor 436 | var pointElRoot = this.pointEl.closest('.' + opt.rootClass), 437 | isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id'); 438 | 439 | /** 440 | * move vertical 441 | */ 442 | if (!mouse.dirAx || isNewRoot || isEmpty) { 443 | // check if groups match if dragging over new root 444 | if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) { 445 | return; 446 | } 447 | // check depth limit 448 | depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length; 449 | if (depth > opt.maxDepth) { 450 | return; 451 | } 452 | var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2); 453 | parent = this.placeEl.parent(); 454 | // if empty create new list to replace empty placeholder 455 | if (isEmpty) { 456 | list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass); 457 | list.append(this.placeEl); 458 | this.pointEl.replaceWith(list); 459 | } 460 | else if (before) { 461 | this.pointEl.before(this.placeEl); 462 | } 463 | else { 464 | this.pointEl.after(this.placeEl); 465 | } 466 | if (!parent.children().length) { 467 | this.unsetParent(parent.parent()); 468 | } 469 | if (!this.dragRootEl.find(opt.itemNodeName).length) { 470 | this.dragRootEl.append('
'); 471 | } 472 | // parent root list has changed 473 | if (isNewRoot) { 474 | this.dragRootEl = pointElRoot; 475 | this.hasNewRoot = this.el[0] !== this.dragRootEl[0]; 476 | } 477 | } 478 | } 479 | 480 | }; 481 | 482 | $.fn.nestable = function(params) 483 | { 484 | var lists = this, 485 | retval = this; 486 | 487 | lists.each(function() 488 | { 489 | var plugin = $(this).data("nestable"); 490 | 491 | if (!plugin) { 492 | $(this).data("nestable", new Plugin(this, params)); 493 | $(this).data("nestable-id", new Date().getTime()); 494 | } else { 495 | if (typeof params === 'string' && typeof plugin[params] === 'function') { 496 | retval = plugin[params](); 497 | } 498 | } 499 | }); 500 | 501 | return retval || lists; 502 | }; 503 | 504 | })(window.jQuery || window.Zepto, window, document); 505 | --------------------------------------------------------------------------------