├── .gitignore ├── .scruntinizer.yml ├── LICENSE ├── README.md ├── composer.json ├── composer.lock └── src ├── actions ├── AdjacencyList │ ├── FullTreeDataAction.php │ ├── PartialTreeDataAction.php │ ├── TreeNodeMoveAction.php │ └── TreeNodesReorderAction.php └── nestedset │ ├── FullTreeDataAction.php │ └── NodeMoveAction.php ├── helpers └── ContextMenuHelper.php ├── messages └── ru │ ├── jstw.defaults.php │ └── jstw.php └── widgets ├── JsTreeAssetBundle.php ├── TreeInput.php ├── TreeInputAssetBundle.php ├── TreeWidget.php ├── tree-input-src └── styles.css └── views └── tree-input.php /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | -------------------------------------------------------------------------------- /.scruntinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | dependencies: 3 | before: 4 | - composer global require "fxp/composer-asset-plugin:~1.1.0" 5 | 6 | tools: 7 | php_sim: true 8 | php_pdepend: true 9 | php_analyzer: true 10 | 11 | checks: 12 | php: 13 | code_rating: true 14 | duplication: true 15 | variable_existence: true 16 | useless_calls: true 17 | use_statement_alias_conflict: true 18 | unused_variables: true 19 | unused_properties: true 20 | unused_parameters: true 21 | unused_methods: true 22 | unreachable_code: true 23 | sql_injection_vulnerabilities: true 24 | security_vulnerabilities: true 25 | precedence_mistakes: true 26 | precedence_in_conditions: true 27 | parameter_non_unique: true 28 | no_property_on_interface: true 29 | no_non_implemented_abstract_methods: true 30 | deprecated_code_usage: true 31 | closure_use_not_conflicting: true 32 | closure_use_modifiable: true 33 | avoid_useless_overridden_methods: true 34 | avoid_conflicting_incrementers: true 35 | assignment_of_null_return: true 36 | use_self_instead_of_fqcn: true 37 | uppercase_constants: true 38 | too_many_arguments: true 39 | simplify_boolean_return: true 40 | scope_indentation: 41 | spaces_per_level: '4' 42 | return_doc_comments: true 43 | return_doc_comment_if_not_inferrable: true 44 | require_scope_for_properties: true 45 | require_scope_for_methods: true 46 | require_braces_around_control_structures: true 47 | psr2_switch_declaration: true 48 | psr2_control_structure_declaration: true 49 | psr2_class_declaration: true 50 | prefer_unix_line_ending: true 51 | php5_style_constructor: true 52 | parameter_doc_comments: true 53 | param_doc_comment_if_not_inferrable: true 54 | overriding_private_members: true 55 | one_class_per_file: true 56 | no_unnecessary_if: true 57 | no_underscore_prefix_in_properties: true 58 | no_underscore_prefix_in_methods: true 59 | no_space_inside_cast_operator: true 60 | no_space_between_concatenation_operator: true 61 | no_space_before_semicolon: true 62 | no_space_around_object_operator: true 63 | no_short_open_tag: true 64 | no_long_variable_names: 65 | maximum: '30' 66 | no_goto: true 67 | no_global_keyword: true 68 | no_eval: true 69 | no_error_suppression: true 70 | no_empty_statements: true 71 | no_duplicate_arguments: true 72 | no_debug_code: true 73 | more_specific_types_in_doc_comments: true 74 | missing_arguments: true 75 | method_calls_on_non_object: true 76 | lowercase_php_keywords: true 77 | lowercase_basic_constants: true 78 | line_length: 79 | max_length: '120' 80 | instanceof_class_exists: true 81 | fix_use_statements: 82 | remove_unused: true 83 | preserve_multiple: false 84 | preserve_blanklines: false 85 | order_alphabetically: true 86 | foreach_traversable: true 87 | foreach_usable_as_reference: true 88 | fix_linefeed: true 89 | ensure_lower_case_builtin_functions: true 90 | encourage_shallow_comparison: true 91 | encourage_postdec_operator: true 92 | deadlock_detection_in_loops: true 93 | catch_class_exists: true 94 | blank_line_after_namespace_declaration: true 95 | avoid_usage_of_logical_operators: true 96 | avoid_unnecessary_concatenation: true 97 | avoid_todo_comments: true 98 | avoid_multiple_statements_on_same_line: true 99 | avoid_length_functions_in_loops: true 100 | avoid_fixme_comments: true 101 | avoid_duplicate_types: true 102 | avoid_corrupting_byteorder_marks: true 103 | avoid_aliased_php_functions: true 104 | argument_type_checks: true 105 | verify_argument_usable_as_reference: true 106 | spacing_of_function_arguments: true 107 | single_namespace_per_use: true 108 | optional_parameters_at_the_end: true 109 | non_commented_empty_catch_block: true 110 | function_body_start_on_new_line: true 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 DevGroup.ru 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-jstree-widget 2 | ================== 3 | [![Code Climate](https://codeclimate.com/github/DevGroup-ru/yii2-jstree-widget/badges/gpa.svg)](https://codeclimate.com/github/DevGroup-ru/yii2-jstree-widget) 4 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/551833bd-1951-493d-9a8f-9f676cf58506/mini.png)](https://insight.sensiolabs.com/projects/551833bd-1951-493d-9a8f-9f676cf58506) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/DevGroup-ru/yii2-jstree-widget/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/DevGroup-ru/yii2-jstree-widget/?branch=master) 6 | [![Build Status](https://scrutinizer-ci.com/g/DevGroup-ru/yii2-jstree-widget/badges/build.png?b=master)](https://scrutinizer-ci.com/g/DevGroup-ru/yii2-jstree-widget/build-status/master) 7 | 8 | 9 | jsTree tree widget for yii2. 10 | 11 | Current state: **unstable**. 12 | 13 | Description 14 | ----------- 15 | 16 | This extension allows you to display and manage hierarchical data structures from your 17 | database using [jsTree](https://www.jstree.com/). 18 | 19 | For now following data structure types are supported: 20 | - [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list); 21 | - [nested set](https://en.wikipedia.org/wiki/Nested_set_model). 22 | 23 | 24 | Usage example 25 | ------------- 26 | For example, we have model Menu that represents our structured data. And MenuController for management purposes. 27 | 28 | Adjacency List 29 | -------------- 30 | In the MenuController: 31 | 32 | ``` php 33 | use devgroup\JsTreeWidget\actions\AdjacencyList\FullTreeDataAction; 34 | use devgroup\JsTreeWidget\actions\AdjacencyList\TreeNodesReorderAction; 35 | use devgroup\JsTreeWidget\actions\AdjacencyList\TreeNodeMoveAction; 36 | ... 37 | public function actions() 38 | { 39 | return [ 40 | 'getTree' => [ 41 | 'class' => FullTreeDataAction::class, 42 | 'className' => Menu::class, 43 | ], 44 | 'menuReorder' => [ 45 | 'class' => TreeNodesReorderAction::class, 46 | 'className' => Menu::class, 47 | ], 48 | 'menuChangeParent' => [ 49 | 'class' => TreeNodeMoveAction::class, 50 | 'className' => Menu::class, 51 | ], 52 | ]; 53 | } 54 | ``` 55 | 56 | In your view file call the widget in the right place: 57 | 58 | ``` php 59 | ['/menu/getTree', 'selected_id' => $parent_id], 61 | 'reorderAction' => ['/menu/menuReorder'], 62 | 'changeParentAction' => ['/menu/menuChangeParent'], 63 | 'treeType' => TreeWidget::TREE_TYPE_ADJACENCY, 64 | 'contextMenuItems' => [ 65 | 'open' => [ 66 | 'label' => 'Open', 67 | 'action' => ContextMenuHelper::actionUrl( 68 | ['/menu/list'], 69 | ['parent_id'] 70 | ), 71 | ], 72 | 'edit' => [ 73 | 'label' => 'Edit', 74 | 'action' => ContextMenuHelper::actionUrl( 75 | ['/menu/edit'] 76 | ), 77 | ] 78 | ], 79 | ]) ?> 80 | ``` 81 | Getting Data, Reordering and Change Parent actions has default implementations, but you can implement and use your own ones, just by changing a routes `'treeDataRoute', 'reorderAction', 'changeParentAction'`. 82 | 83 | Nested Set 84 | ---------- 85 | Nested set can work in single or multy root modes. Single root mode by default. 86 | For using multi root mode you have to have `tree` (or other name you like) column in your database table to store root id. And define this name in all necessary config places (see below). 87 | 88 | In the MenuController: 89 | 90 | ``` php 91 | use devgroup\JsTreeWidget\actions\nestedset\FullTreeDataAction; 92 | use devgroup\JsTreeWidget\actions\nestedset\NodeMoveAction; 93 | ... 94 | public function actions() 95 | { 96 | return [ 97 | 'getTree' => [ 98 | 'class' => FullTreeDataAction::class, 99 | 'className' => Menu::class, 100 | 'rootAttribute' => 'tree', //omit for single root mode 101 | ], 102 | 'treeReorder' => [ 103 | 'class' => NodeMoveAction::class, 104 | 'className' => Menu::class, 105 | 'rootAttribute' => 'tree', //omit for single root mode 106 | ], 107 | ]; 108 | } 109 | ``` 110 | In the view file: 111 | ```php 112 | ['/menu/getTree'], 114 | 'reorderAction' => ['/menu/treeReorder'], 115 | 'treeType' => TreeWidget::TREE_TYPE_NESTED_SET, //important config option 116 | 'contextMenuItems' => [ 117 | 'edit' => [ 118 | 'label' => 'Edit', 119 | 'action' => ContextMenuHelper::actionUrl( 120 | ['/menu/edit'] 121 | ), 122 | ] 123 | ], 124 | ]) ?> 125 | ``` 126 | Getting Data and Node Movements actions has the default implementations and are independent from side `NestedSet behaviors`. But you also can use your own implementation. 127 | 128 | `TreeWidget` will register bundle `JsTreeAssetBundle`, but you may want to include it as dependency in your main bundle(ie. for minification purpose). 129 | 130 | `ContextMenuHelper` creates `JsExpression` for handling context menu option click. It automatically adds all `data` attributes from item link(`` tag) if it is not specified exactly(as in 'open' menu item). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devgroup/yii2-jstree-widget", 3 | "description": "jsTree widget for yii2", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","jstree","tree","widget","nested-sets"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Alexander Kozhevnikov", 10 | "email": "b37hr3z3n@gmail.com" 11 | }, 12 | { 13 | "name": "Evgeny Dubovitsky", 14 | "email": "flynn068@gmail.com" 15 | }, 16 | { 17 | "name": "Pavel Fedotov", 18 | "email": "fps.06@mail.ru" 19 | }, 20 | { 21 | "name": "Pavel Naumov", 22 | "email": "huggbies@email.su" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "require": { 27 | "yiisoft/yii2": "~2.0.0", 28 | "bower-asset/jstree": "*", 29 | "devgroup/yii2-tag-dependency-helper": "~1.5" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "devgroup\\JsTreeWidget\\": "src/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "91dec5aa15975245c2b7286daa5b6acb", 8 | "content-hash": "91c83f95c43126f178464e2f95b8e56c", 9 | "packages": [ 10 | { 11 | "name": "bower-asset/jquery", 12 | "version": "2.2.4", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/jquery/jquery-dist.git", 16 | "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/c0185ab7c75aab88762c5aae780b9d83b80eda72", 21 | "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72", 22 | "shasum": "" 23 | }, 24 | "type": "bower-asset-library", 25 | "extra": { 26 | "bower-asset-main": "dist/jquery.js", 27 | "bower-asset-ignore": [ 28 | "package.json" 29 | ] 30 | }, 31 | "license": [ 32 | "MIT" 33 | ], 34 | "keywords": [ 35 | "browser", 36 | "javascript", 37 | "jquery", 38 | "library" 39 | ] 40 | }, 41 | { 42 | "name": "bower-asset/jquery.inputmask", 43 | "version": "3.2.7", 44 | "source": { 45 | "type": "git", 46 | "url": "https://github.com/RobinHerbots/jquery.inputmask.git", 47 | "reference": "5a72c563b502b8e05958a524cdfffafe9987be38" 48 | }, 49 | "dist": { 50 | "type": "zip", 51 | "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/5a72c563b502b8e05958a524cdfffafe9987be38", 52 | "reference": "5a72c563b502b8e05958a524cdfffafe9987be38", 53 | "shasum": "" 54 | }, 55 | "require": { 56 | "bower-asset/jquery": ">=1.7" 57 | }, 58 | "type": "bower-asset-library", 59 | "extra": { 60 | "bower-asset-main": [ 61 | "./dist/inputmask/inputmask.js" 62 | ], 63 | "bower-asset-ignore": [ 64 | "**/*", 65 | "!dist/*", 66 | "!dist/inputmask/*", 67 | "!dist/min/*", 68 | "!dist/min/inputmask/*", 69 | "!extra/bindings/*", 70 | "!extra/dependencyLibs/*", 71 | "!extra/phone-codes/*" 72 | ] 73 | }, 74 | "license": [ 75 | "http://opensource.org/licenses/mit-license.php" 76 | ], 77 | "description": "jquery.inputmask is a jquery plugin which create an input mask.", 78 | "keywords": [ 79 | "form", 80 | "input", 81 | "inputmask", 82 | "jquery", 83 | "mask", 84 | "plugins" 85 | ] 86 | }, 87 | { 88 | "name": "bower-asset/jstree", 89 | "version": "dev-master", 90 | "source": { 91 | "type": "git", 92 | "url": "https://github.com/vakata/jstree.git", 93 | "reference": "bb0473ae8cfc205585b6404ef86f650df2f6996e" 94 | }, 95 | "dist": { 96 | "type": "zip", 97 | "url": "https://api.github.com/repos/vakata/jstree/zipball/bb0473ae8cfc205585b6404ef86f650df2f6996e", 98 | "reference": "bb0473ae8cfc205585b6404ef86f650df2f6996e", 99 | "shasum": "" 100 | }, 101 | "require": { 102 | "bower-asset/jquery": ">=1.9.1" 103 | }, 104 | "type": "bower-asset-library", 105 | "extra": { 106 | "bower-asset-main": [ 107 | "./dist/jstree.js", 108 | "./dist/themes/default/style.css" 109 | ], 110 | "bower-asset-ignore": [ 111 | "**/.*", 112 | "docs", 113 | "demo", 114 | "libs", 115 | "node_modules", 116 | "test", 117 | "libs", 118 | "jstree.jquery.json", 119 | "gruntfile.js", 120 | "package.json", 121 | "bower.json", 122 | "component.json", 123 | "LICENCE-MIT", 124 | "README.md" 125 | ], 126 | "branch-alias": { 127 | "dev-master": "3.3.2-dev" 128 | } 129 | }, 130 | "keywords": [ 131 | "jstree", 132 | "tree", 133 | "ui" 134 | ] 135 | }, 136 | { 137 | "name": "bower-asset/punycode", 138 | "version": "v1.3.2", 139 | "source": { 140 | "type": "git", 141 | "url": "https://github.com/bestiejs/punycode.js.git", 142 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" 143 | }, 144 | "dist": { 145 | "type": "zip", 146 | "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", 147 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", 148 | "shasum": "" 149 | }, 150 | "type": "bower-asset-library", 151 | "extra": { 152 | "bower-asset-main": "punycode.js", 153 | "bower-asset-ignore": [ 154 | "coverage", 155 | "tests", 156 | ".*", 157 | "component.json", 158 | "Gruntfile.js", 159 | "node_modules", 160 | "package.json" 161 | ] 162 | } 163 | }, 164 | { 165 | "name": "bower-asset/yii2-pjax", 166 | "version": "v2.0.6", 167 | "source": { 168 | "type": "git", 169 | "url": "https://github.com/yiisoft/jquery-pjax.git", 170 | "reference": "60728da6ade5879e807a49ce59ef9a72039b8978" 171 | }, 172 | "dist": { 173 | "type": "zip", 174 | "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/60728da6ade5879e807a49ce59ef9a72039b8978", 175 | "reference": "60728da6ade5879e807a49ce59ef9a72039b8978", 176 | "shasum": "" 177 | }, 178 | "require": { 179 | "bower-asset/jquery": ">=1.8" 180 | }, 181 | "type": "bower-asset-library", 182 | "extra": { 183 | "bower-asset-main": "./jquery.pjax.js", 184 | "bower-asset-ignore": [ 185 | ".travis.yml", 186 | "Gemfile", 187 | "Gemfile.lock", 188 | "CONTRIBUTING.md", 189 | "vendor/", 190 | "script/", 191 | "test/" 192 | ] 193 | }, 194 | "license": [ 195 | "MIT" 196 | ] 197 | }, 198 | { 199 | "name": "cebe/markdown", 200 | "version": "dev-master", 201 | "source": { 202 | "type": "git", 203 | "url": "https://github.com/cebe/markdown.git", 204 | "reference": "04bfcaa26356cf86c6c4a2420eb95857a86e03ab" 205 | }, 206 | "dist": { 207 | "type": "zip", 208 | "url": "https://api.github.com/repos/cebe/markdown/zipball/04bfcaa26356cf86c6c4a2420eb95857a86e03ab", 209 | "reference": "04bfcaa26356cf86c6c4a2420eb95857a86e03ab", 210 | "shasum": "" 211 | }, 212 | "require": { 213 | "lib-pcre": "*", 214 | "php": ">=5.4.0" 215 | }, 216 | "require-dev": { 217 | "cebe/indent": "*", 218 | "facebook/xhprof": "*@dev", 219 | "phpunit/phpunit": "4.1.*" 220 | }, 221 | "bin": [ 222 | "bin/markdown" 223 | ], 224 | "type": "library", 225 | "extra": { 226 | "branch-alias": { 227 | "dev-master": "1.1.x-dev" 228 | } 229 | }, 230 | "autoload": { 231 | "psr-4": { 232 | "cebe\\markdown\\": "" 233 | } 234 | }, 235 | "notification-url": "https://packagist.org/downloads/", 236 | "license": [ 237 | "MIT" 238 | ], 239 | "authors": [ 240 | { 241 | "name": "Carsten Brandt", 242 | "email": "mail@cebe.cc", 243 | "homepage": "http://cebe.cc/", 244 | "role": "Creator" 245 | } 246 | ], 247 | "description": "A super fast, highly extensible markdown parser for PHP", 248 | "homepage": "https://github.com/cebe/markdown#readme", 249 | "keywords": [ 250 | "extensible", 251 | "fast", 252 | "gfm", 253 | "markdown", 254 | "markdown-extra" 255 | ], 256 | "time": "2016-06-20 21:09:53" 257 | }, 258 | { 259 | "name": "devgroup/yii2-tag-dependency-helper", 260 | "version": "1.4.0", 261 | "source": { 262 | "type": "git", 263 | "url": "https://github.com/DevGroup-ru/yii2-tag-dependency-helper.git", 264 | "reference": "c52149f3a718f57e5335697734fb1151aa0bf3c2" 265 | }, 266 | "dist": { 267 | "type": "zip", 268 | "url": "https://api.github.com/repos/DevGroup-ru/yii2-tag-dependency-helper/zipball/c52149f3a718f57e5335697734fb1151aa0bf3c2", 269 | "reference": "c52149f3a718f57e5335697734fb1151aa0bf3c2", 270 | "shasum": "" 271 | }, 272 | "require": { 273 | "yiisoft/yii2": "~2.0.6" 274 | }, 275 | "require-dev": { 276 | "phpunit/dbunit": "~1.0", 277 | "phpunit/phpunit": "~4.0" 278 | }, 279 | "type": "yii2-extension", 280 | "autoload": { 281 | "psr-4": { 282 | "DevGroup\\TagDependencyHelper\\": "src/" 283 | } 284 | }, 285 | "notification-url": "https://packagist.org/downloads/", 286 | "license": [ 287 | "MIT" 288 | ], 289 | "authors": [ 290 | { 291 | "name": "Alexander Kozhevnikov", 292 | "email": "b37hr3z3n@gmail.com" 293 | }, 294 | { 295 | "name": "Evgeny Dubovitsky", 296 | "email": "flynn068@gmail.com" 297 | }, 298 | { 299 | "name": "Pavel Fedotov", 300 | "email": "fps.06@mail.ru" 301 | }, 302 | { 303 | "name": "Salikh Fakhrutdinov", 304 | "email": "me@free6k.ru" 305 | } 306 | ], 307 | "description": "Helper for unifying cache tag names with invalidation support in yii2", 308 | "keywords": [ 309 | "cache", 310 | "yii2" 311 | ], 312 | "time": "2016-01-15 09:29:24" 313 | }, 314 | { 315 | "name": "ezyang/htmlpurifier", 316 | "version": "v4.8.0", 317 | "source": { 318 | "type": "git", 319 | "url": "https://github.com/ezyang/htmlpurifier.git", 320 | "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2" 321 | }, 322 | "dist": { 323 | "type": "zip", 324 | "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", 325 | "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", 326 | "shasum": "" 327 | }, 328 | "require": { 329 | "php": ">=5.2" 330 | }, 331 | "type": "library", 332 | "autoload": { 333 | "psr-0": { 334 | "HTMLPurifier": "library/" 335 | }, 336 | "files": [ 337 | "library/HTMLPurifier.composer.php" 338 | ] 339 | }, 340 | "notification-url": "https://packagist.org/downloads/", 341 | "license": [ 342 | "LGPL" 343 | ], 344 | "authors": [ 345 | { 346 | "name": "Edward Z. Yang", 347 | "email": "admin@htmlpurifier.org", 348 | "homepage": "http://ezyang.com" 349 | } 350 | ], 351 | "description": "Standards compliant HTML filter written in PHP", 352 | "homepage": "http://htmlpurifier.org/", 353 | "keywords": [ 354 | "html" 355 | ], 356 | "time": "2016-07-16 12:58:58" 357 | }, 358 | { 359 | "name": "yiisoft/yii2", 360 | "version": "dev-master", 361 | "source": { 362 | "type": "git", 363 | "url": "https://github.com/yiisoft/yii2-framework.git", 364 | "reference": "3e56a83e49deacebec94cecb0ccd6d0977b20565" 365 | }, 366 | "dist": { 367 | "type": "zip", 368 | "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/3e56a83e49deacebec94cecb0ccd6d0977b20565", 369 | "reference": "3e56a83e49deacebec94cecb0ccd6d0977b20565", 370 | "shasum": "" 371 | }, 372 | "require": { 373 | "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", 374 | "bower-asset/jquery.inputmask": "~3.2.2", 375 | "bower-asset/punycode": "1.3.*", 376 | "bower-asset/yii2-pjax": "~2.0.1", 377 | "cebe/markdown": "~1.0.0 | ~1.1.0", 378 | "ext-ctype": "*", 379 | "ext-mbstring": "*", 380 | "ezyang/htmlpurifier": "~4.6", 381 | "lib-pcre": "*", 382 | "php": ">=5.4.0", 383 | "yiisoft/yii2-composer": "~2.0.4" 384 | }, 385 | "bin": [ 386 | "yii" 387 | ], 388 | "type": "library", 389 | "extra": { 390 | "branch-alias": { 391 | "dev-master": "2.0.x-dev" 392 | } 393 | }, 394 | "autoload": { 395 | "psr-4": { 396 | "yii\\": "" 397 | } 398 | }, 399 | "notification-url": "https://packagist.org/downloads/", 400 | "license": [ 401 | "BSD-3-Clause" 402 | ], 403 | "authors": [ 404 | { 405 | "name": "Qiang Xue", 406 | "email": "qiang.xue@gmail.com", 407 | "homepage": "http://www.yiiframework.com/", 408 | "role": "Founder and project lead" 409 | }, 410 | { 411 | "name": "Alexander Makarov", 412 | "email": "sam@rmcreative.ru", 413 | "homepage": "http://rmcreative.ru/", 414 | "role": "Core framework development" 415 | }, 416 | { 417 | "name": "Maurizio Domba", 418 | "homepage": "http://mdomba.info/", 419 | "role": "Core framework development" 420 | }, 421 | { 422 | "name": "Carsten Brandt", 423 | "email": "mail@cebe.cc", 424 | "homepage": "http://cebe.cc/", 425 | "role": "Core framework development" 426 | }, 427 | { 428 | "name": "Timur Ruziev", 429 | "email": "resurtm@gmail.com", 430 | "homepage": "http://resurtm.com/", 431 | "role": "Core framework development" 432 | }, 433 | { 434 | "name": "Paul Klimov", 435 | "email": "klimov.paul@gmail.com", 436 | "role": "Core framework development" 437 | }, 438 | { 439 | "name": "Dmitry Naumenko", 440 | "email": "d.naumenko.a@gmail.com", 441 | "role": "Core framework development" 442 | } 443 | ], 444 | "description": "Yii PHP Framework Version 2", 445 | "homepage": "http://www.yiiframework.com/", 446 | "keywords": [ 447 | "framework", 448 | "yii2" 449 | ], 450 | "time": "2016-08-17 08:58:09" 451 | }, 452 | { 453 | "name": "yiisoft/yii2-composer", 454 | "version": "dev-master", 455 | "source": { 456 | "type": "git", 457 | "url": "https://github.com/yiisoft/yii2-composer.git", 458 | "reference": "e882cc327a6935a41f2e8665b9ef0317f6852d4d" 459 | }, 460 | "dist": { 461 | "type": "zip", 462 | "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/e882cc327a6935a41f2e8665b9ef0317f6852d4d", 463 | "reference": "e882cc327a6935a41f2e8665b9ef0317f6852d4d", 464 | "shasum": "" 465 | }, 466 | "require": { 467 | "composer-plugin-api": "^1.0" 468 | }, 469 | "type": "composer-plugin", 470 | "extra": { 471 | "class": "yii\\composer\\Plugin", 472 | "branch-alias": { 473 | "dev-master": "2.0.x-dev" 474 | } 475 | }, 476 | "autoload": { 477 | "psr-4": { 478 | "yii\\composer\\": "" 479 | } 480 | }, 481 | "notification-url": "https://packagist.org/downloads/", 482 | "license": [ 483 | "BSD-3-Clause" 484 | ], 485 | "authors": [ 486 | { 487 | "name": "Qiang Xue", 488 | "email": "qiang.xue@gmail.com" 489 | } 490 | ], 491 | "description": "The composer plugin for Yii extension installer", 492 | "keywords": [ 493 | "composer", 494 | "extension installer", 495 | "yii2" 496 | ], 497 | "time": "2016-06-05 20:34:32" 498 | } 499 | ], 500 | "packages-dev": [], 501 | "aliases": [], 502 | "minimum-stability": "dev", 503 | "stability-flags": [], 504 | "prefer-stable": false, 505 | "prefer-lowest": false, 506 | "platform": [], 507 | "platform-dev": [] 508 | } 509 | -------------------------------------------------------------------------------- /src/actions/AdjacencyList/FullTreeDataAction.php: -------------------------------------------------------------------------------- 1 | [ 23 | * 'class' => AdjacencyFullTreeDataAction::class, 24 | * 'className' => Category::class, 25 | * 'modelLabelAttribute' => 'defaultTranslation.name', 26 | * 27 | * ], 28 | * ... 29 | * ]; 30 | * } 31 | * ``` 32 | */ 33 | class FullTreeDataAction extends Action 34 | { 35 | 36 | public $className = null; 37 | 38 | public $modelIdAttribute = 'id'; 39 | 40 | public $modelLabelAttribute = 'name'; 41 | 42 | public $modelParentAttribute = 'parent_id'; 43 | 44 | public $varyByTypeAttribute = null; 45 | 46 | public $queryParentAttribute = 'id'; 47 | 48 | public $querySortOrder = 'sort_order'; 49 | 50 | public $querySelectedAttribute = 'selected_id'; 51 | /** 52 | * Additional conditions for retrieving tree(ie. don't display nodes marked as deleted) 53 | * @var array|\Closure 54 | */ 55 | public $whereCondition = []; 56 | 57 | /** 58 | * Cache key prefix. Should be unique if you have multiple actions with different $whereCondition 59 | * @var string|\Closure 60 | */ 61 | public $cacheKey = 'FullTree'; 62 | 63 | /** 64 | * Cache lifetime for the full tree 65 | * @var int 66 | */ 67 | public $cacheLifeTime = 86400; 68 | 69 | private $selectedNodes = []; 70 | 71 | public function init() 72 | { 73 | if (!isset($this->className)) { 74 | throw new InvalidConfigException("Model name should be set in controller actions"); 75 | } 76 | if (!class_exists($this->className)) { 77 | throw new InvalidConfigException("Model class does not exists"); 78 | } 79 | } 80 | 81 | public function run() 82 | { 83 | Yii::$app->response->format = Response::FORMAT_JSON; 84 | 85 | /** @var \yii\db\ActiveRecord $class */ 86 | $class = $this->className; 87 | 88 | if (null === $current_selected_id = Yii::$app->request->get($this->querySelectedAttribute)) { 89 | $current_selected_id = Yii::$app->request->get($this->queryParentAttribute); 90 | } 91 | $cacheKey = $this->cacheKey instanceof \Closure ? call_user_func($this->cacheKey) : $this->cacheKey; 92 | $cacheKey = "AdjacencyFullTreeData:{$cacheKey}:{$class}:{$this->querySortOrder}"; 93 | 94 | Yii::beginProfile('Get tree'); 95 | if (false === $result = Yii::$app->cache->get($cacheKey)) { 96 | Yii::beginProfile('Build tree'); 97 | $query = $class::find() 98 | ->orderBy([$this->querySortOrder => SORT_ASC]); 99 | 100 | if ($this->whereCondition instanceof \Closure) { 101 | $query->where(call_user_func($this->whereCondition)); 102 | } elseif (count($this->whereCondition) > 0) { 103 | $query->where($this->whereCondition); 104 | } 105 | 106 | if (null === $rows = $query->asArray()->all()) { 107 | return []; 108 | } 109 | 110 | $result = []; 111 | 112 | foreach ($rows as $row) { 113 | $parent = ArrayHelper::getValue($row, $this->modelParentAttribute, 0); 114 | $item = [ 115 | 'id' => ArrayHelper::getValue($row, $this->modelIdAttribute, 0), 116 | 'parent' => ($parent) ? $parent : '#', 117 | 'text' => ArrayHelper::getValue($row, $this->modelLabelAttribute, 'item'), 118 | 'a_attr' => [ 119 | 'data-id' => $row[$this->modelIdAttribute], 120 | 'data-parent_id' => $row[$this->modelParentAttribute] 121 | ], 122 | ]; 123 | 124 | if (null !== $this->varyByTypeAttribute) { 125 | $item['type'] = $row[$this->varyByTypeAttribute]; 126 | } 127 | 128 | $result[$row[$this->modelIdAttribute]] = $item; 129 | } 130 | 131 | Yii::$app->cache->set( 132 | $cacheKey, 133 | $result, 134 | 86400, 135 | new TagDependency([ 136 | 'tags' => [ 137 | NamingHelper::getCommonTag($class), 138 | ], 139 | ]) 140 | ); 141 | 142 | Yii::endProfile('Build tree'); 143 | } 144 | 145 | if (array_key_exists($current_selected_id, $result)) { 146 | $result[$current_selected_id] = array_merge( 147 | $result[$current_selected_id], 148 | ['state' => ['opened' => true, 'selected' => true]] 149 | ); 150 | } 151 | $this->selectedNodes = explode(',', Yii::$app->request->get('selected', '')); 152 | foreach ($this->selectedNodes as $node) { 153 | if ($node !== '') { 154 | if (array_key_exists($node, $result)) { 155 | $result[$node]['state'] = [ 156 | 'selected' => true, 157 | ]; 158 | } 159 | } 160 | } 161 | Yii::endProfile('Get tree'); 162 | 163 | Yii::$app->response->format = Response::FORMAT_RAW; 164 | header('Content-Type: application/json'); 165 | return json_encode(array_values($result)); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/actions/AdjacencyList/PartialTreeDataAction.php: -------------------------------------------------------------------------------- 1 | [ 22 | * 'class' => PartialTreeDataAction::class, 23 | * 'className' => Category::class, 24 | * 'modelLabelAttribute' => 'defaultTranslation.name', 25 | * 26 | * ], 27 | * ... 28 | * ]; 29 | * } 30 | * ``` 31 | */ 32 | class PartialTreeDataAction extends Action 33 | { 34 | 35 | public $className; 36 | 37 | public $modelIdAttribute = 'id'; 38 | 39 | public $modelLabelAttribute = 'name'; 40 | 41 | public $modelParentAttribute = 'parent_id'; 42 | 43 | public $varyByTypeAttribute; 44 | 45 | public $queryParentAttribute = 'id'; 46 | 47 | public $querySortOrder = 'sort_order'; 48 | 49 | public $querySelectedAttribute = 'selected_id'; 50 | /** 51 | * Additional conditions for retrieving tree(ie. don't display nodes marked as deleted) 52 | * @var array|\Closure 53 | */ 54 | public $whereCondition = []; 55 | 56 | /** 57 | * Cache key prefix. Should be unique if you have multiple actions with different $whereCondition 58 | * @var string|\Closure 59 | */ 60 | public $cacheKey = 'PartialTree'; 61 | 62 | /** 63 | * Cache lifetime for the full tree 64 | * @var int 65 | */ 66 | public $cacheLifeTime = 86400; 67 | 68 | public function init() 69 | { 70 | if ($this->className === null) { 71 | throw new InvalidConfigException('Model name should be set in controller actions'); 72 | } 73 | if (!class_exists($this->className)) { 74 | throw new InvalidConfigException('Model class does not exists'); 75 | } 76 | } 77 | 78 | public function run() 79 | { 80 | Yii::$app->response->format = Response::FORMAT_JSON; 81 | 82 | /** @var \yii\db\ActiveRecord $class */ 83 | $class = $this->className; 84 | 85 | if (null === $current_selected_id = Yii::$app->request->get($this->querySelectedAttribute)) { 86 | $current_selected_id = Yii::$app->request->get($this->queryParentAttribute); 87 | } 88 | 89 | $parent_id = Yii::$app->request->get($this->queryParentAttribute, '#'); 90 | if (!is_numeric($parent_id)) { 91 | $parent_id = 0; 92 | } 93 | 94 | $cacheKey = $this->cacheKey instanceof \Closure ? call_user_func($this->cacheKey) : $this->cacheKey; 95 | 96 | $cacheKey = "AdjacencyFullTreeData:$cacheKey:{$class}:{$this->querySortOrder}:$parent_id"; 97 | 98 | if (false === $result = Yii::$app->cache->get($cacheKey)) { 99 | $query = $class::find() 100 | ->orderBy([$this->querySortOrder => SORT_ASC]); 101 | 102 | if ($this->whereCondition instanceof \Closure) { 103 | $query->where(call_user_func($this->whereCondition)); 104 | } elseif (count($this->whereCondition) > 0) { 105 | $query->where($this->whereCondition); 106 | } 107 | $query->andWhere([$this->modelParentAttribute => $parent_id]); 108 | 109 | if (null === $rows = $query->asArray()->all()) { 110 | return []; 111 | } 112 | 113 | $result = []; 114 | 115 | foreach ($rows as $row) { 116 | $parent = ArrayHelper::getValue($row, $this->modelParentAttribute, 0); 117 | $item = [ 118 | 'id' => ArrayHelper::getValue($row, $this->modelIdAttribute, 0), 119 | 'parent' => $parent ?: '#', 120 | 'text' => ArrayHelper::getValue($row, $this->modelLabelAttribute, 'item'), 121 | 'a_attr' => [ 122 | 'data-id' => $row[$this->modelIdAttribute], 123 | 'data-parent_id' => $row[$this->modelParentAttribute] 124 | ], 125 | 'children' => true, 126 | ]; 127 | 128 | if (null !== $this->varyByTypeAttribute) { 129 | $item['type'] = $row[$this->varyByTypeAttribute]; 130 | } 131 | 132 | $result[$row[$this->modelIdAttribute]] = $item; 133 | } 134 | 135 | Yii::$app->cache->set( 136 | $cacheKey, 137 | $result, 138 | 86400, 139 | new TagDependency([ 140 | 'tags' => [ 141 | NamingHelper::getCommonTag($class), 142 | ], 143 | ]) 144 | ); 145 | } 146 | 147 | if (array_key_exists($current_selected_id, $result)) { 148 | $result[$current_selected_id] = array_merge( 149 | $result[$current_selected_id], 150 | ['state' => ['opened' => true, 'selected' => true]] 151 | ); 152 | } 153 | 154 | return array_values($result); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/actions/AdjacencyList/TreeNodeMoveAction.php: -------------------------------------------------------------------------------- 1 | [ 21 | * 'class' => TreeNodeMoveAction::class, 22 | * 'class_name' => Category::class, 23 | * ], 24 | * ... 25 | * ]; 26 | * } 27 | * ``` 28 | */ 29 | 30 | class TreeNodeMoveAction extends Action 31 | { 32 | public $className = null; 33 | public $modelParentIdField = 'parent_id'; 34 | public $parentId = null; 35 | public $saveAttributes = []; 36 | 37 | public function init() 38 | { 39 | if (!isset($this->className)) { 40 | throw new InvalidConfigException("Model name should be set in controller actions"); 41 | } 42 | if (!class_exists($this->className)) { 43 | throw new InvalidConfigException("Model class does not exists"); 44 | } 45 | if (!in_array($this->modelParentIdField, $this->saveAttributes)) { 46 | $this->saveAttributes[] = $this->modelParentIdField; 47 | } 48 | } 49 | 50 | public function run($id = null) 51 | { 52 | $this->parentId = Yii::$app->request->get('parent_id'); 53 | $class = $this->className; 54 | if (null === $id 55 | || null === $this->parentId 56 | || (null === $model = $class::findOne($id)) 57 | || (null === $parent = $class::findOne($this->parentId))) { 58 | throw new NotFoundHttpException; 59 | } 60 | /** @var ActiveRecord $model */ 61 | $model->{$this->modelParentIdField} = $parent->id; 62 | TagDependency::invalidate( 63 | Yii::$app->cache, 64 | NamingHelper::getCommonTag($class) 65 | ); 66 | return $model->save(true, $this->saveAttributes); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/actions/AdjacencyList/TreeNodesReorderAction.php: -------------------------------------------------------------------------------- 1 | [ 21 | * 'class' => TreeNodesReorderAction::class, 22 | * 'class_name' => Category::class, 23 | * ], 24 | * ... 25 | * ]; 26 | * } 27 | * ``` 28 | */ 29 | class TreeNodesReorderAction extends Action 30 | { 31 | public $className = null; 32 | public $modelSortOrderField = 'sort_order'; 33 | public $sortOrder = []; 34 | 35 | public function init() 36 | { 37 | if (!isset($this->className)) { 38 | throw new InvalidConfigException("Model name should be set in controller actions"); 39 | } 40 | if (!class_exists($this->className)) { 41 | throw new InvalidConfigException("Model class does not exists"); 42 | } 43 | $this->sortOrder = Yii::$app->request->post('order'); 44 | if (empty($this->sortOrder)) { 45 | throw new BadRequestHttpException; 46 | } 47 | } 48 | 49 | public function run() 50 | { 51 | /** @var ActiveRecord $class */ 52 | $class = $this->className; 53 | $sortOrderField = Yii::$app->db->quoteColumnName($this->modelSortOrderField); 54 | $case = 'CASE `id`'; 55 | $newSortOrders = []; 56 | foreach ($this->sortOrder as $id => $sort_order) { 57 | if ($sort_order === '' || $sort_order === null) { 58 | continue; 59 | } 60 | $case .= ' when "' . $id . '" then "' . $sort_order . '"'; 61 | $newSortOrders[$id] = $sort_order; 62 | } 63 | $case .= ' END'; 64 | $sql = "UPDATE " 65 | . $class::tableName() 66 | . " SET " . $sortOrderField . " = " 67 | . $case 68 | . " WHERE `id` IN(" . implode(', ', array_keys($newSortOrders)) 69 | . ")"; 70 | TagDependency::invalidate( 71 | Yii::$app->cache, 72 | NamingHelper::getCommonTag($class) 73 | ); 74 | return 0 !== Yii::$app->db->createCommand($sql)->execute(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/actions/nestedset/FullTreeDataAction.php: -------------------------------------------------------------------------------- 1 | className) || false === is_subclass_of($this->className, ActiveRecord::class)) { 40 | throw new InvalidConfigException('"className" param must be set and must be child of ActiveRecord'); 41 | } 42 | /** @var ActiveRecord $class */ 43 | $class = $this->className; 44 | $this->tableName = $class::tableName(); 45 | $scheme = Yii::$app->getDb()->getTableSchema($this->tableName); 46 | $columns = $scheme->columns; 47 | if (false !== $this->rootAttribute && false === isset($columns[$this->rootAttribute])) { 48 | throw new InvalidConfigException("Column '{$this->rootAttribute}' not found in the '{$this->tableName}' table"); 49 | } 50 | if (false === isset( 51 | $columns[$this->leftAttribute], 52 | $columns[$this->rightAttribute], 53 | $columns[$this->modelLabelAttribute] 54 | ) 55 | ) { 56 | throw new InvalidConfigException( 57 | "Some of the '{$this->leftAttribute}', '{$this->rightAttribute}', '{$this->modelLabelAttribute}', " 58 | . "not found in the '{$this->tableName}' columns list" 59 | ); 60 | } 61 | TreeWidget::registerTranslations(); 62 | parent::init(); 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | public function run() 69 | { 70 | $selectArray = [ 71 | 'id', 72 | $this->leftAttribute, 73 | $this->rightAttribute, 74 | $this->modelLabelAttribute, 75 | ]; 76 | $orderBy = []; 77 | if (false !== $this->rootAttribute) { 78 | $selectArray[] = $this->rootAttribute; 79 | $orderBy[$this->rootAttribute] = SORT_ASC; 80 | } 81 | $orderBy[$this->leftAttribute] = SORT_ASC; 82 | Yii::$app->response->format = Response::FORMAT_JSON; 83 | $data = (new Query()) 84 | ->from($this->tableName) 85 | ->select($selectArray) 86 | ->orderBy($orderBy) 87 | ->all(); 88 | 89 | $this->selectedNodes = explode(',', Yii::$app->request->get('selected', '')); 90 | 91 | return $this->prepareNestedData($data); 92 | } 93 | 94 | /** 95 | * Converts single or multi root Nested Set database data into multidimensional array for using in the 96 | * jsTree widget 97 | * 98 | * @param array $data 99 | * @param int $lft 100 | * @param null $rgt 101 | * @param int $root 102 | * @return array 103 | */ 104 | public function prepareNestedData($data, $lft = 0, $rgt = null, $root = 0) 105 | { 106 | $res = []; 107 | foreach ($data as $row) { 108 | $currentRoot = isset($row[$this->rootAttribute]) ? $row[$this->rootAttribute] : 0; 109 | if (is_null($rgt) || $row[$this->rightAttribute] < $rgt && $root == $currentRoot) { 110 | if ($lft + 1 == $row[$this->leftAttribute]) { 111 | if ($row[$this->leftAttribute] + 1 !== $row[$this->rightAttribute]) { 112 | $res[] = [ 113 | 'id' => $row['id'], 114 | 'text' => $row[$this->modelLabelAttribute], 115 | 'a_attr' => [ 116 | 'data-id' => $row['id'], 117 | ], 118 | 'children' => self::prepareNestedData( 119 | $data, 120 | $row[$this->leftAttribute], 121 | $row[$this->rightAttribute], 122 | $currentRoot 123 | ), 124 | 'state' => [ 125 | 'selected' => in_array($row['id'], $this->selectedNodes), 126 | ], 127 | ]; 128 | } else { 129 | $res[] = [ 130 | 'id' => $row['id'], 131 | 'text' => $row[$this->modelLabelAttribute], 132 | 'a_attr' => [ 133 | 'data-id' => $row['id'], 134 | ], 135 | 'children' => [], 136 | 'state' => [ 137 | 'selected' => in_array($row['id'], $this->selectedNodes), 138 | ], 139 | ]; 140 | } 141 | $lft = $row[$this->rightAttribute]; 142 | } else if ($row[$this->leftAttribute] == 1 && $root !== $currentRoot) { 143 | $res[] = [ 144 | 'id' => $row['id'], 145 | 'text' => $row[$this->modelLabelAttribute], 146 | 'a_attr' => [ 147 | 'data-id' => $row['id'], 148 | ], 149 | 'children' => self::prepareNestedData( 150 | $data, 151 | $row[$this->leftAttribute], 152 | $row[$this->rightAttribute], 153 | $currentRoot 154 | ), 155 | 'state' => [ 156 | 'selected' => in_array($row['id'], $this->selectedNodes), 157 | ], 158 | ]; 159 | } 160 | } 161 | } 162 | return $res; 163 | } 164 | } -------------------------------------------------------------------------------- /src/actions/nestedset/NodeMoveAction.php: -------------------------------------------------------------------------------- 1 | className) || false === is_subclass_of($this->className, ActiveRecord::class)) { 46 | throw new InvalidConfigException('"className" param must be set and must be child of ActiveRecord'); 47 | } 48 | /** @var ActiveRecord $class */ 49 | $class = $this->className; 50 | $this->tableName = $class::tableName(); 51 | $scheme = Yii::$app->getDb()->getTableSchema($this->tableName); 52 | $columns = $scheme->columns; 53 | if (false !== $this->rootAttribute && false === isset($columns[$this->rootAttribute])) { 54 | throw new InvalidConfigException("Column '{$this->rootAttribute}' not found in the '{$this->tableName}' table"); 55 | } 56 | if (false === isset( 57 | $columns[$this->leftAttribute], 58 | $columns[$this->rightAttribute], 59 | $columns[$this->depthAttribute] 60 | ) 61 | ) { 62 | throw new InvalidConfigException( 63 | "Some of the '{$this->leftAttribute}', '{$this->rightAttribute}', '{$this->depthAttribute}', " 64 | . "not found in the '{$this->tableName}' columns list" 65 | ); 66 | } 67 | TreeWidget::registerTranslations(); 68 | parent::init(); 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function run() 75 | { 76 | Yii::$app->response->format = Response::FORMAT_JSON; 77 | $newParentId = Yii::$app->request->post('parent'); 78 | $oldParentId = Yii::$app->request->post('old_parent'); 79 | $position = Yii::$app->request->post('position'); 80 | $oldPosition = Yii::$app->request->post('old_position'); 81 | $nodeId = Yii::$app->request->post('node_id'); 82 | $siblings = Yii::$app->request->post('siblings', []); 83 | $class = $this->className; 84 | if ((int)$newParentId == 0) { 85 | return ['error' => Yii::t('jstw', 'Can not move node as root!')]; 86 | } 87 | if ((null === $node = $class::findOne($nodeId)) || (null === $parent = $class::findOne($newParentId))) { 88 | return ['error' => Yii::t('jstw', 'Invalid node id or parent id received!')]; 89 | } 90 | if ($this->depthLimit) { 91 | $nodeMaxDepth = $node->children()->select(new Expression('MAX(' . $this->depthAttribute . ')'))->scalar(); 92 | $nodeMaxDepth = $nodeMaxDepth ? $nodeMaxDepth : $node->{$this->depthAttribute}; 93 | $nodeResultDepth = $parent->{$this->depthAttribute} + ($nodeMaxDepth - $node->{$this->depthAttribute} + 1); 94 | if ($nodeResultDepth >= $this->depthLimit) { 95 | return ['error' => Yii::t('jstw', 'Can not move node because max depth ({depthLimit}) is exceeded!', ['depthLimit' => $this->depthLimit])]; 96 | } 97 | } 98 | $this->node = $node; 99 | $this->parent = $parent; 100 | if (false !== $this->rootAttribute && ($node->{$this->rootAttribute} != $parent->{$this->rootAttribute})) { 101 | return $this->moveMultiRoot($position, $siblings, $oldParentId); 102 | } 103 | if ($newParentId == $oldParentId) { 104 | return $this->reorder($oldPosition, $position, $siblings); 105 | } else { 106 | return $this->move($position, $siblings, $oldParentId); 107 | } 108 | } 109 | 110 | /** 111 | * Moves node inside one parent inside one root 112 | * 113 | * @param null $oldPosition 114 | * @param null $position 115 | * @param array $siblings 116 | * @return array|bool 117 | * @throws \yii\db\Exception 118 | */ 119 | protected function reorder($oldPosition = null, $position = null, $siblings = []) 120 | { 121 | if (null === $oldPosition || null === $position || true === empty($siblings)) { 122 | return ['error' => Yii::t('jstw', 'Invalid data provided!')]; 123 | } 124 | $nodeId = $siblings[$position]; 125 | $class = $this->className; 126 | if ($oldPosition > $position) { 127 | //change next 128 | $nodeOperator = '-'; 129 | $siblingsOperator = '+'; 130 | $workWith = array_slice($siblings, $position, $oldPosition - $position + 1); 131 | } else if ($oldPosition < $position) { 132 | //change previous 133 | $nodeOperator = '+'; 134 | $siblingsOperator = '-'; 135 | $workWith = array_slice($siblings, $oldPosition, $position - $oldPosition + 1); 136 | } else { 137 | return true; 138 | } 139 | if (true === empty($workWith)) { 140 | return ['error' => Yii::t('jstw', 'Invalid data provided!')]; 141 | } 142 | $lr = $workWithLr = $this->getLr($workWith); 143 | if (true === empty($lr)) { 144 | return ['error' => Yii::t('jstw', 'Invalid data provided!')]; 145 | } 146 | unset($workWithLr[$nodeId]); 147 | $lft = array_column($workWithLr, $this->leftAttribute); 148 | $lft = min($lft); 149 | $rgt = array_column($workWithLr, $this->rightAttribute); 150 | $rgt = max($rgt); 151 | $nodeCondition = [ 152 | 'and', 153 | ['>=', $this->leftAttribute, $lft], 154 | ['<=', $this->rightAttribute, $rgt] 155 | ]; 156 | $this->applyRootCondition($nodeCondition); 157 | $nodeDelta = $this->getCount($nodeCondition); 158 | $nodeDelta *= 2; 159 | $siblingsCondition = [ 160 | 'and', 161 | ['>=', $this->leftAttribute, $lr[$nodeId][$this->leftAttribute]], 162 | ['<=', $this->rightAttribute, $lr[$nodeId][$this->rightAttribute]] 163 | ]; 164 | $this->applyRootCondition($siblingsCondition); 165 | $nodeChildren = $this->getChildIds($siblingsCondition); 166 | $siblingsDelta = count($nodeChildren) * 2; 167 | $db = Yii::$app->getDb(); 168 | $transaction = $db->beginTransaction(); 169 | try { 170 | //updating necessary node siblings 171 | $db->createCommand()->update( 172 | $class::tableName(), 173 | [ 174 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('%s%d', $siblingsOperator, $siblingsDelta)), 175 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('%s%d', $siblingsOperator, $siblingsDelta)), 176 | ], 177 | $nodeCondition 178 | )->execute(); 179 | //updating node 180 | $db->createCommand()->update( 181 | $class::tableName(), 182 | [ 183 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 184 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 185 | ], 186 | ['id' => $nodeChildren] 187 | )->execute(); 188 | $transaction->commit(); 189 | } catch (\Exception $e) { 190 | $transaction->rollBack(); 191 | return ['error' => $e->getMessage()]; 192 | } 193 | return true; 194 | } 195 | 196 | /** 197 | * Moves node inside one root 198 | * 199 | * @param null $position 200 | * @param array $siblings 201 | * @param string | integer $oldParentId 202 | * @return array|bool 203 | * @throws \yii\db\Exception 204 | */ 205 | protected function move($position = null, $siblings = [], $oldParentId) 206 | { 207 | $class = $this->className; 208 | if (null === $oldParent = $class::findOne($oldParentId)) { 209 | return ['error' => Yii::t('jstw', "Old parent with id '{id}' not found!", ['id' => $oldParentId])]; 210 | } 211 | $nodeCountCondition = [ 212 | 'and', 213 | ['>=', $this->leftAttribute, $this->node{$this->leftAttribute}], 214 | ['<=', $this->rightAttribute, $this->node{$this->rightAttribute}] 215 | ]; 216 | $this->applyRootCondition($nodeCountCondition); 217 | $nodeChildren = $this->getChildIds($nodeCountCondition); 218 | $siblingsDelta = count($nodeChildren) * 2; 219 | if ($position == 0) { 220 | $compareRight = $this->parent->{$this->leftAttribute} + 1; 221 | } else { 222 | if (false === isset($siblings[$position - 1])) { 223 | return ['error' => Yii::t('jstw', 'New previous sibling does not exist')]; 224 | } 225 | $newPrevSiblingId = $siblings[$position - 1]; 226 | $newPrevSiblingData = $this->getLr($newPrevSiblingId); 227 | $compareRight = $newPrevSiblingData[$newPrevSiblingId][$this->rightAttribute]; 228 | } 229 | if ($this->node->{$this->leftAttribute} > $compareRight) { 230 | //move node up 231 | if ($position == 0) { 232 | $leftFrom = $compareRight; 233 | } else { 234 | $leftFrom = $compareRight + 1; 235 | } 236 | $rightTo = $this->node->{$this->leftAttribute}; 237 | $nodeDelta = $this->node->{$this->leftAttribute} - $leftFrom; 238 | $nodeOperator = '-'; 239 | $parentOperator = $siblingsOperator = '+'; 240 | $newParentUpdateField = $this->rightAttribute; 241 | $oldParentUpdateField = $this->leftAttribute; 242 | } else if ($this->node->{$this->leftAttribute} < $compareRight) { 243 | //move node down 244 | $leftFrom = $this->node->{$this->rightAttribute}; 245 | if ($position == 0) { 246 | $rightTo = $compareRight - 1; 247 | } else { 248 | $rightTo = $compareRight; 249 | } 250 | $nodeOperator = '+'; 251 | $parentOperator = $siblingsOperator = '-'; 252 | $nodeDelta = $rightTo - $siblingsDelta + 1 - $this->node->{$this->leftAttribute}; 253 | $newParentUpdateField = $this->leftAttribute; 254 | $oldParentUpdateField = $this->rightAttribute; 255 | } else { 256 | return ['error' => Yii::t('jstw', 'There are two nodes with same "left" value. This should not be.')]; 257 | } 258 | $siblingsCondition = [ 259 | 'and', 260 | ['>=', $this->leftAttribute, $leftFrom], 261 | ['<=', $this->rightAttribute, $rightTo] 262 | ]; 263 | $this->applyRootCondition($siblingsCondition); 264 | $db = Yii::$app->getDb(); 265 | $transaction = $db->beginTransaction(); 266 | $oldParentDepth = $oldParent->{$this->depthAttribute}; 267 | $newParentDepth = $this->parent->{$this->depthAttribute}; 268 | if ($newParentDepth < $oldParentDepth) { 269 | $depthOperator = '-'; 270 | $depthDelta = $oldParentDepth - $newParentDepth; 271 | } else { 272 | $depthOperator = '+'; 273 | $depthDelta = $newParentDepth - $oldParentDepth; 274 | } 275 | $commonParentsCondition = [ 276 | 'and', 277 | ['<', $this->leftAttribute, $leftFrom], 278 | ['>', $this->rightAttribute, $rightTo] 279 | ]; 280 | $this->applyRootCondition($commonParentsCondition); 281 | $commonParentsIds = $class::find()->select('id')->where($commonParentsCondition)->column(); 282 | $commonCondition = [ 283 | ['!=', $this->depthAttribute, 0], 284 | ['not in', 'id', $commonParentsIds], 285 | ]; 286 | $this->applyRootCondition($commonCondition); 287 | $newParentCondition = array_merge([ 288 | 'and', 289 | ['<=', $this->leftAttribute, $this->parent->{$this->leftAttribute}], 290 | ['>=', $this->rightAttribute, $this->parent->{$this->rightAttribute}], 291 | ], $commonCondition); 292 | $oldParentsCondition = array_merge([ 293 | 'and', 294 | ['<', $this->leftAttribute, $this->node->{$this->leftAttribute}], 295 | ['>', $this->rightAttribute, $this->node->{$this->rightAttribute}], 296 | ], $commonCondition); 297 | try { 298 | //updating necessary node siblings 299 | $db->createCommand()->update( 300 | $class::tableName(), 301 | [ 302 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('%s%d', $siblingsOperator, $siblingsDelta)), 303 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('%s%d', $siblingsOperator, $siblingsDelta)), 304 | ], 305 | $siblingsCondition 306 | )->execute(); 307 | //updating old parents 308 | $db->createCommand()->update( 309 | $class::tableName(), 310 | [ 311 | //down - right 312 | $oldParentUpdateField => new Expression($oldParentUpdateField . sprintf('%s%d', $parentOperator, $siblingsDelta)), 313 | ], 314 | $oldParentsCondition 315 | )->execute(); 316 | //updating new parents 317 | $db->createCommand()->update( 318 | $class::tableName(), 319 | [ 320 | //down - left 321 | $newParentUpdateField => new Expression($newParentUpdateField . sprintf('%s%d', $parentOperator, $siblingsDelta)), 322 | ], 323 | $newParentCondition 324 | )->execute(); 325 | //updating node with children 326 | $db->createCommand()->update( 327 | $class::tableName(), 328 | [ 329 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 330 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 331 | $this->depthAttribute => new Expression($this->depthAttribute . sprintf('%s%d', $depthOperator, $depthDelta)), 332 | ], 333 | ['id' => $nodeChildren] 334 | )->execute(); 335 | $transaction->commit(); 336 | } catch (\Exception $e) { 337 | $transaction->rollBack(); 338 | return ['error' => $e->getMessage()]; 339 | } 340 | return true; 341 | } 342 | 343 | /** 344 | * Moves node between two roots 345 | * 346 | * @param null $position 347 | * @param array $siblings 348 | * @param string | integer $oldParentId 349 | * @return array|bool 350 | * @throws \yii\db\Exception 351 | */ 352 | protected function moveMultiRoot($position = null, $siblings = [], $oldParentId) 353 | { 354 | $class = $this->className; 355 | if ((int)$oldParentId == 0) { 356 | return ['error' => Yii::t('jstw', 'Can not move root node as child!')]; 357 | } 358 | if (null === $oldParent = $class::findOne($oldParentId)) { 359 | return ['error' => Yii::t('jstw', "Old parent with id '{id}' not found!", ['id' => $oldParentId])]; 360 | } 361 | $nodeCountCondition = [ 362 | 'and', 363 | ['>=', $this->leftAttribute, $this->node->{$this->leftAttribute}], 364 | ['<=', $this->rightAttribute, $this->node->{$this->rightAttribute}], 365 | [$this->rootAttribute => $this->node->{$this->rootAttribute}] 366 | ]; 367 | $nodeChildren = $this->getChildIds($nodeCountCondition); 368 | $siblingsDelta = count($nodeChildren) * 2; 369 | if ($position == 0) { 370 | $leftFrom = $this->parent->{$this->leftAttribute} + 1; 371 | } else { 372 | if (false === isset($siblings[$position - 1])) { 373 | return ['error' => Yii::t('jstw', 'New previous sibling does not exist')]; 374 | } 375 | $newPrevSiblingId = $siblings[$position - 1]; 376 | $newPrevSiblingData = $this->getLr($newPrevSiblingId); 377 | $leftFrom = $newPrevSiblingData[$newPrevSiblingId][$this->rightAttribute] + 1; 378 | } 379 | if ($this->node->{$this->leftAttribute} > $leftFrom) { 380 | $nodeDelta = $this->node->{$this->leftAttribute} - $leftFrom; 381 | $nodeOperator = '-'; 382 | } else { 383 | $nodeDelta = $leftFrom - $this->node->{$this->leftAttribute}; 384 | $nodeOperator = '+'; 385 | } 386 | $siblingsCondition = [ 387 | 'and', 388 | ['>=', $this->leftAttribute, $leftFrom], 389 | [$this->rootAttribute => $this->parent->{$this->rootAttribute}] 390 | ]; 391 | $oldSiblingsCondition = [ 392 | 'and', 393 | ['>', $this->leftAttribute, $this->node->{$this->rightAttribute}], 394 | [$this->rootAttribute => $this->node->{$this->rootAttribute}] 395 | ]; 396 | $db = Yii::$app->getDb(); 397 | $transaction = $db->beginTransaction(); 398 | $oldParentDepth = $oldParent->{$this->depthAttribute}; 399 | $newParentDepth = $this->parent->{$this->depthAttribute}; 400 | if ($newParentDepth < $oldParentDepth) { 401 | $depthOperator = '-'; 402 | $depthDelta = $oldParentDepth - $newParentDepth; 403 | } else { 404 | $depthOperator = '+'; 405 | $depthDelta = $newParentDepth - $oldParentDepth; 406 | } 407 | $newParentCondition = [ 408 | 'and', 409 | ['<=', $this->leftAttribute, $this->parent->{$this->leftAttribute}], 410 | ['>=', $this->rightAttribute, $this->parent->{$this->rightAttribute}], 411 | [$this->rootAttribute => $this->parent->{$this->rootAttribute}] 412 | ]; 413 | $oldParentsCondition = [ 414 | 'and', 415 | ['<=', $this->leftAttribute, $oldParent->{$this->leftAttribute}], 416 | ['>=', $this->rightAttribute, $oldParent->{$this->rightAttribute}], 417 | [$this->rootAttribute => $oldParent->{$this->rootAttribute}] 418 | ]; 419 | try { 420 | //updating necessary node new siblings 421 | $db->createCommand()->update( 422 | $class::tableName(), 423 | [ 424 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('+%d', $siblingsDelta)), 425 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('+%d', $siblingsDelta)), 426 | ], 427 | $siblingsCondition 428 | )->execute(); 429 | //updating necessary node old siblings 430 | $db->createCommand()->update( 431 | $class::tableName(), 432 | [ 433 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('-%d', $siblingsDelta)), 434 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('-%d', $siblingsDelta)), 435 | ], 436 | $oldSiblingsCondition 437 | )->execute(); 438 | //updating old parents 439 | $db->createCommand()->update( 440 | $class::tableName(), 441 | [ 442 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('-%d', $siblingsDelta)), 443 | ], 444 | $oldParentsCondition 445 | )->execute(); 446 | //updating new parents 447 | $db->createCommand()->update( 448 | $class::tableName(), 449 | [ 450 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('+%d', $siblingsDelta)), 451 | ], 452 | $newParentCondition 453 | )->execute(); 454 | //updating node with children 455 | $db->createCommand()->update( 456 | $class::tableName(), 457 | [ 458 | $this->leftAttribute => new Expression($this->leftAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 459 | $this->rightAttribute => new Expression($this->rightAttribute . sprintf('%s%d', $nodeOperator, $nodeDelta)), 460 | $this->depthAttribute => new Expression($this->depthAttribute . sprintf('%s%d', $depthOperator, $depthDelta)), 461 | $this->rootAttribute => $this->parent->{$this->rootAttribute} 462 | ], 463 | ['id' => $nodeChildren] 464 | )->execute(); 465 | $transaction->commit(); 466 | } catch (\Exception $e) { 467 | $transaction->rollBack(); 468 | return ['error' => $e->getMessage()]; 469 | } 470 | return true; 471 | } 472 | 473 | /** 474 | * Returns field set of rows to be modified while reordering 475 | * 476 | * @param array $ids 477 | * @return array|\yii\db\ActiveRecord[] 478 | */ 479 | protected function getLr($ids) 480 | { 481 | $class = $this->className; 482 | return $class::find() 483 | ->select(['id', $this->leftAttribute, $this->rightAttribute]) 484 | ->where(['id' => $ids]) 485 | ->indexBy('id') 486 | ->asArray(true) 487 | ->all(); 488 | } 489 | 490 | /** 491 | * Returns count of records to be modified while reordering 492 | * 493 | * @param array $condition 494 | * @return int|string 495 | */ 496 | protected function getCount($condition) 497 | { 498 | $class = $this->className; 499 | return $class::find() 500 | ->select(['id', $this->leftAttribute, $this->rightAttribute, $this->rootAttribute]) 501 | ->where($condition) 502 | ->count(); 503 | } 504 | 505 | 506 | /** 507 | * Returns child ids of selected node 508 | * 509 | * @param array $condition 510 | * @return array 511 | */ 512 | protected function getChildIds($condition) 513 | { 514 | $class = $this->className; 515 | return $class::find() 516 | ->select('id') 517 | ->where($condition) 518 | ->column(); 519 | } 520 | 521 | /** 522 | * Applies tree root condition if multi root 523 | * 524 | * @param $condition 525 | */ 526 | protected function applyRootCondition(&$condition) 527 | { 528 | if (false !== $this->rootAttribute) { 529 | $condition[] = [$this->rootAttribute => $this->node->{$this->rootAttribute}]; 530 | } 531 | } 532 | } -------------------------------------------------------------------------------- /src/helpers/ContextMenuHelper.php: -------------------------------------------------------------------------------- 1 | 0 ? '&' : '?'; 26 | 27 | $dataExpression = "var data = \$object.data(), dataVariables = [];"; 28 | 29 | if (is_array($dataAttributes) === true) { 30 | // only selected set of attributes 31 | foreach ($dataAttributes as $attribute => $match) { 32 | if (is_numeric($attribute) === true) { 33 | $attribute = $match; 34 | } 35 | $jsonAttribute = Json::encode($attribute); 36 | $matchAttribute = Json::encode($match); 37 | 38 | $dataExpression .= " 39 | if (typeof(data[$matchAttribute]) !== 'undefined') { 40 | dataVariables.push( '$attribute=' + encodeURIComponent(data[$matchAttribute]) ); 41 | }\n"; 42 | 43 | } 44 | } elseif ($dataAttributes === true) { 45 | // all attributes 46 | $dataExpression .= " 47 | for (var attributeName in data) { 48 | dataVariables.push(encodeURIComponent(attributeName) + '=' + encodeURIComponent(data[attributeName])); 49 | }\n"; 50 | } else { 51 | $dataExpression = "var dataVariables = '';"; 52 | } 53 | $dataExpression .= "dataVariables=dataVariables.join('&'); "; 54 | return new JsExpression(" 55 | function(node) { 56 | var \$object = node.reference ? \$(node.reference[0]) : node; 57 | $dataExpression 58 | document.location = $baseUrl + '$union' + dataVariables; 59 | return false; 60 | } 61 | "); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/messages/ru/jstw.defaults.php: -------------------------------------------------------------------------------- 1 | 'Открыть', 4 | 'Edit' => 'Редактировать', 5 | 'Delete' => 'Удалить', 6 | 'Publish' => 'Опубликовать', 7 | 'Unpublish' => 'Снять с публикации', 8 | 'Activate' => 'Активировать', 9 | 'Deactivate' => 'Деактивировать', 10 | 'Show' => 'Показать', 11 | 'Hide' => 'Скрыть', 12 | 'Preview' => 'Просмотреть', 13 | 'Create' => 'Создать', 14 | ]; -------------------------------------------------------------------------------- /src/messages/ru/jstw.php: -------------------------------------------------------------------------------- 1 | 'Нельзя сделать дочерний элемент корневым!', 4 | 'Invalid node id or parent id received!' => 'Получены неврные id родительского или перемещаемого элемента!', 5 | 'Can not move node because max depth ({depthLimit}) is exceeded!' => 'Нельзя переместить элемент потому что превышена максимальная вложенность ({depthLimit})!', 6 | 'Invalid data provided!' => 'Получены неверные данные!', 7 | "Old parent with id '{id}' not found!" => "Старый родительский элемент с id '{id}' не найден!", 8 | 'New previous sibling does not exist' => 'Невозможно определить нового предыдущего соседа!', 9 | 'There are two nodes with same "left" value. This should not be.' => 'Два элемента с одинаковым значением левого атрибута. Такого не должно быть.', 10 | 'Can not move root node as child!' => 'Нельзя сделать корневой элемент дочерним!', 11 | ]; -------------------------------------------------------------------------------- /src/widgets/JsTreeAssetBundle.php: -------------------------------------------------------------------------------- 1 | selectText === null) { 26 | $this->selectText = Yii::t('jstw', 'Select'); 27 | } 28 | } 29 | 30 | public function run() 31 | { 32 | $id = 'input_tree__' . $this->getId(); 33 | $this->options['id'] = $id; 34 | 35 | 36 | if ($this->hasModel()) { 37 | $input = Html::activeHiddenInput($this->model, $this->attribute, $this->options); 38 | $value = Html::getAttributeValue($this->model, $this->attribute); 39 | } else { 40 | $input = Html::hiddenInput($this->name, $this->value, $this->options); 41 | $value = $this->value; 42 | } 43 | if (is_array($value)) { 44 | $value = implode(',', $value); 45 | } 46 | $this->treeConfig['selectedNodes'] = $value; 47 | 48 | $this->treeConfig['id'] = $id . '__tree'; 49 | $oldOptions = isset($this->treeConfig['options']) ? $this->treeConfig['options'] : []; 50 | $this->treeConfig['options'] = ArrayHelper::merge($oldOptions, [ 51 | 'core' => [ 52 | 'multiple' => $this->multiple, 53 | 'dblclick_toggle' => false, 54 | ], 55 | ]); 56 | $this->treeConfig['plugins'] = [ 57 | 'wholerow', 58 | 'contextmenu', 59 | 'dnd', 60 | 'types', 61 | ]; 62 | 63 | if ($this->multiple) { 64 | $this->treeConfig['plugins'][] = 'checkbox'; 65 | } 66 | if ($this->search) { 67 | $this->treeConfig['plugins'][] = 'search'; 68 | $this->treeConfig['options']['search'] = [ 69 | 'show_only_matches' => true, 70 | ]; 71 | } 72 | 73 | return $this->render( 74 | 'tree-input', 75 | [ 76 | 'id' => $id, 77 | 'treeConfig' => $this->treeConfig, 78 | 'multiple' => $this->multiple, 79 | 'selectIcon' => $this->selectIcon, 80 | 'selectText' => $this->selectText, 81 | 'input' => $input, 82 | 'clickToOpen' => $this->clickToOpen, 83 | 'search' => $this->search, 84 | ] 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/widgets/TreeInputAssetBundle.php: -------------------------------------------------------------------------------- 1 | sourcePath = __DIR__ . '/tree-input-src/'; 13 | $this->css = [ 14 | 'styles.css' 15 | ]; 16 | parent::init(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/widgets/TreeWidget.php: -------------------------------------------------------------------------------- 1 | [ 43 | 'icon' => 'fa fa-file-o', 44 | ], 45 | 'list' => [ 46 | 'icon' => 'fa fa-list', 47 | ], 48 | ]; 49 | 50 | /** 51 | * Context menu actions configuration. 52 | * @var array 53 | */ 54 | public $contextMenuItems = []; 55 | 56 | /** 57 | * Various options for jsTree plugin. Will be merged with default options. 58 | * @var array 59 | */ 60 | public $options = []; 61 | 62 | /** 63 | * Route to action which returns json data for tree 64 | * @var array 65 | */ 66 | public $treeDataRoute = null; 67 | 68 | /** 69 | * Translation category for Yii::t() which will be applied to labels. 70 | * If translation is not needed - use false. 71 | */ 72 | public $menuLabelsTranslationCategory = false; 73 | 74 | /** 75 | * JsExpression for action(callback function) on double click. You can use JsExpression or make custom expression. 76 | * Warning! Callback function differs from native jsTree function - it consumes only one attribute - node(similar to contextmenu action). 77 | * Use false if no action needed. 78 | * @var bool|JsExpression 79 | */ 80 | public $doubleClickAction = false; 81 | 82 | /** @var bool|array route to change parent action (applicable to Adjacency List only) */ 83 | public $changeParentAction = false; 84 | 85 | /** @var bool|array route to reorder action */ 86 | public $reorderAction = false; 87 | 88 | /** @var bool plugin config option for allow multiple nodes selections or not */ 89 | public $multiSelect = false; 90 | 91 | /** @var string Default labels translation category */ 92 | private $defaultTranslationCategory = 'jstw.defaults'; 93 | 94 | /** @var array selected nodes */ 95 | public $selectedNodes = []; 96 | 97 | /** 98 | * @inheritdoc 99 | */ 100 | public function init() 101 | { 102 | if (false === $this->menuLabelsTranslationCategory) { 103 | $this->menuLabelsTranslationCategory = $this->defaultTranslationCategory; 104 | } 105 | self::registerTranslations(); 106 | parent::init(); 107 | } 108 | 109 | public static function registerTranslations() 110 | { 111 | Yii::$app->i18n->translations['jstw*'] = [ 112 | 'class' => 'yii\i18n\PhpMessageSource', 113 | 'basePath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'messages', 114 | ]; 115 | } 116 | 117 | /** 118 | * @inheritdoc 119 | */ 120 | public function run() 121 | { 122 | if (!is_array($this->treeDataRoute)) { 123 | throw new InvalidConfigException("Attribute treeDataRoute is required to use TreeWidget."); 124 | } 125 | 126 | if (count($this->selectedNodes) > 0) { 127 | $this->treeDataRoute['selected'] = $this->selectedNodes; 128 | } 129 | 130 | $options = [ 131 | 'plugins' => $this->plugins, 132 | 'core' => [ 133 | 'check_callback' => true, 134 | 'multiple' => $this->multiSelect, 135 | 'data' => [ 136 | 'url' => new JsExpression( 137 | "function (node) { 138 | return " . Json::encode(Url::to($this->treeDataRoute)) . "; 139 | }" 140 | ), 141 | 'success' => new JsExpression( 142 | "function (node) { 143 | return { 'id' : node.id }; 144 | }" 145 | ), 146 | 'data' => new JsExpression( 147 | "function (node) { 148 | return { 'id' : node.id }; 149 | }" 150 | ), 151 | 'error' => new JsExpression( 152 | "function ( o, textStatus, errorThrown ) { 153 | alert(o.responseText); 154 | }" 155 | ) 156 | ] 157 | ] 158 | ]; 159 | 160 | // merge with attribute-provided options 161 | $options = ArrayHelper::merge($options, $this->options); 162 | if (false === empty($this->contextMenuItems)) { 163 | if (!in_array('contextmenu', $this->plugins)) { 164 | // add missing contextmenu plugin 165 | $options['plugins'] = ['contextmenu']; 166 | } 167 | $functionName = $this->getId() . 'ContextMenu'; 168 | $options['contextmenu'] = ['items' => new JsExpression($functionName), 'select_node' => false]; 169 | $this->contextMenuOptions($functionName); 170 | } 171 | $options = Json::encode($options); 172 | $this->getView()->registerAssetBundle('devgroup\JsTreeWidget\widgets\JsTreeAssetBundle'); 173 | 174 | $doubleClick = ''; 175 | if ($this->doubleClickAction !== false) { 176 | $doubleClick = " 177 | jsTree_{$this->getId()}.on('dblclick.jstree', function (e) { 178 | var node = $(e.target).closest('.jstree-node').children('.jstree-anchor'); 179 | var callback = " . $this->doubleClickAction . "; 180 | callback(node); 181 | return false; 182 | });\n"; 183 | } 184 | $treeJs = $this->prepareJs(); 185 | $this->getView()->registerJs(" 186 | var jsTree_{$this->getId()} = \$('#{$this->getId()}').jstree($options); 187 | $doubleClick $treeJs", View::POS_READY); 188 | return Html::tag('div', '', ['id' => $this->getId()]); 189 | } 190 | 191 | /** 192 | * @param $functionName 193 | * @return array 194 | */ 195 | private function contextMenuOptions($functionName) 196 | { 197 | $items = []; 198 | $conditionItems = ""; 199 | foreach ($this->contextMenuItems as $index => $item) { 200 | $item['label'] = Yii::t($this->menuLabelsTranslationCategory, $item['label']); 201 | if (false === empty($item['showWhen'])) { 202 | if (true === is_array($item['showWhen'])) { 203 | $condition = []; 204 | foreach ($item['showWhen'] as $key => $value) { 205 | $key = (false !== strpos($key, 'data-')) ? $key : 'data-' . $key; 206 | $condition[] = "node.hasOwnProperty('a_attr') && node.a_attr['$key'] == {$value}"; 207 | } 208 | $condition = implode(' && ', $condition); 209 | } else { 210 | $condition = $item['showWhen']; 211 | } 212 | unset($item['showWhen']); 213 | $item = Json::encode($item); 214 | $conditionItems .= new JsExpression(" 215 | if ({$condition}) { 216 | items.{$index} = $item; 217 | } 218 | "); 219 | } else { 220 | $items[$index] = $item; 221 | } 222 | } 223 | $items = Json::encode($items); 224 | $js = <<view->registerJs($js, View::POS_HEAD); 232 | } 233 | 234 | /** 235 | * Prepares js according to given tree type 236 | * 237 | * @return string 238 | */ 239 | private function prepareJs() 240 | { 241 | switch ($this->treeType) { 242 | case self::TREE_TYPE_ADJACENCY : 243 | return $this->adjacencyJs(); 244 | case self::TREE_TYPE_NESTED_SET : 245 | return $this->nestedSetJs(); 246 | } 247 | } 248 | 249 | /** 250 | * @return string 251 | */ 252 | private function adjacencyJs() 253 | { 254 | $changeParentJs = ''; 255 | if ($this->changeParentAction !== false) { 256 | $changeParentUrl = is_array($this->changeParentAction) ? Url::to($this->changeParentAction) : $this->changeParentAction; 257 | $changeParentJs = <<getId()}.on('move_node.jstree', function(e, data) { 259 | var \$this = $(this); 260 | $.get('$changeParentUrl', { 261 | 'id': data.node.id, 262 | 'parent_id': data.parent 263 | }, "json") 264 | .done(function (data) { 265 | if ('undefined' !== typeof(data.error)) { 266 | alert(data.error); 267 | } 268 | \$this.jstree('refresh'); 269 | }) 270 | .fail(function ( o, textStatus, errorThrown ) { 271 | alert(o.responseText); 272 | }); 273 | return false; 274 | }); 275 | JS; 276 | } 277 | 278 | $reorderJs = ''; 279 | if ($this->reorderAction !== false) { 280 | $reorderUrl = is_array($this->reorderAction) ? Url::to($this->reorderAction) : $this->reorderAction; 281 | $reorderJs = <<getId()}.on('move_node.jstree', function(e, data) { 283 | var params = []; 284 | var \$this = $(this); 285 | $('.jstree-node').each(function(i, e) { 286 | params[e.id] = i; 287 | }); 288 | $.post('$reorderUrl', { 289 | 'order':params, 290 | 'id': data.node.id 291 | }, 292 | "json") 293 | .done(function (data) { 294 | if ('undefined' !== typeof(data.error)) { 295 | alert(data.error); 296 | } 297 | \$this.jstree('refresh'); 298 | }) 299 | .fail(function ( o, textStatus, errorThrown ) { 300 | alert(o.responseText); 301 | }); 302 | return false; 303 | }); 304 | JS; 305 | } 306 | return $changeParentJs . "\n" . $reorderJs . "\n"; 307 | } 308 | 309 | /** 310 | * @return string 311 | */ 312 | private function nestedSetJs() 313 | { 314 | $js = ""; 315 | if (false !== $this->reorderAction || false !== $this->changeParentAction) { 316 | $action = $this->reorderAction ?: $this->changeParentAction; 317 | $url = is_array($action) ? Url::to($action) : $action; 318 | $js = <<getId()}.on('move_node.jstree', function(e, data) { 320 | var \$this = $(this), 321 | \$parent = \$this.jstree(true).get_node(data.parent), 322 | \$oldParent = \$this.jstree(true).get_node(data.old_parent), 323 | siblings = \$parent.children || {}; 324 | $.post('$url', { 325 | 'node_id' : data.node.id, 326 | 'parent': data.parent, 327 | 'position': data.position, 328 | 'old_parent': data.old_parent, 329 | 'old_position': data.old_position, 330 | 'is_multi': data.is_multi, 331 | 'siblings': siblings 332 | }, "json") 333 | .done(function (data) { 334 | if ('undefined' !== typeof(data.error)) { 335 | alert(data.error); 336 | } 337 | \$this.jstree('refresh'); 338 | }) 339 | .fail(function ( o, textStatus, errorThrown ) { 340 | alert(o.responseText); 341 | }); 342 | return false; 343 | }); 344 | JS; 345 | } 346 | return $js . "\n"; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/widgets/tree-input-src/styles.css: -------------------------------------------------------------------------------- 1 | .tree-input { 2 | position: relative; 3 | } 4 | .tree-input__tree-container { 5 | position: absolute; 6 | background: #fff; 7 | border: 1px solid #ddd; 8 | width: 100%; 9 | height: 50vh; 10 | overflow: auto; 11 | display: none; 12 | box-shadow: 0 0 10px 1px rgba(0,0,0,0.3); 13 | z-index: 1; 14 | } 15 | .jstree-contextmenu { 16 | z-index: 2; 17 | } 18 | .tree-input__tree-notice { 19 | background: #eee; 20 | border: 1px solid #00a7d0; 21 | padding: 4px; 22 | } 23 | 24 | .tree-input__tree-container_active, .tree-input__tree-container_opened_always { 25 | display: block; 26 | } 27 | .tree-input__tree-container_opened_always { 28 | position: relative; 29 | } 30 | .tree-input__selected-values { 31 | line-height: 1.44; 32 | padding: 6px 12px; 33 | } -------------------------------------------------------------------------------- /src/widgets/views/tree-input.php: -------------------------------------------------------------------------------- 1 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 54 | 55 | 61 | 62 |
63 |
64 | '); 107 | selected.append('
' + path + '
'); 108 | } 109 | 110 | const val = $('#{$id}').val(); 111 | const selectedNow = val.length > 0 ? val.split(',') : []; 112 | selectedNow.push(node); 113 | $('#{$id}').val(selectedNow.join(',')).trigger('change'); 114 | }; 115 | js; 116 | 117 | if ($multiple === false) { 118 | $js .= << 0) { 136 | $('#{$id}__tree').on('ready.jstree', function(e, data) { 137 | emptySelected(); 138 | selectNode(selectedVal); 139 | 140 | }); 141 | } 142 | js; 143 | } else { 144 | $js .= <<registerJs( 184 | <<