├── .gitignore ├── LICENSE ├── Readme.md ├── bower.json ├── example.html ├── json-tree.css ├── json-tree.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .directory 3 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Konstantin Skipor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 5 | and associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 13 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 16 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Editable JSON tree 2 | 3 | [![NPM Version](http://img.shields.io/npm/v/json-tree2.svg?style=flat)](https://www.npmjs.org/package/json-tree2) 4 | 5 | An AngularJS directive used for displaying and editing JSON data in a tree view. It works independently of jQuery (only internal angular's jqLite). 6 | Available operations with nodes: 7 | 8 | * `add` new nodes, 9 | * `reset` node values to null, 10 | * `remove` node completely, 11 | * `change` node value, 12 | * `convert` type of the node (to object, array, string, number, boolean, null, function) implicitly, 13 | * `drag` and `sort` tree nodes (via pressed `Ctrl`). 14 | 15 | ## How to use 16 | 17 | ### Install 18 | 19 | ##### bower 20 | 21 | $ bower install json-tree 22 | 23 | An AngularJS would be installed as a dependency automatically. If it won't, install it manually: 24 | 25 | $ bower install angular 26 | 27 | Add dependencies to the `` section of your main html: 28 | ```html 29 | 30 | 31 | 32 | ``` 33 | 34 | If you don't use bower, you can manually download and unpack json-tree ([zip](https://github.com/krispo/json-tree/archive/v0.1.5.zip), [tar.gz](https://github.com/krispo/json-tree/archive/v0.1.5.tar.gz)). 35 | 36 | ##### npm 37 | 38 | $ npm install json-tree2 39 | 40 | ### Basic usage 41 | 42 | Inject `json-tree` directive into angular module and push some data to the controller: 43 | ```javascript 44 | angular.module('myApp', ['json-tree']) 45 | .controller('myCtrl', function('$scope'){ 46 | $scope.jsonData = { /* JSON data */ }; 47 | }) 48 | ``` 49 | 50 | and in html again you can use it like: 51 | ```html 52 |
53 |
54 | 55 |
56 |
57 | ``` 58 | 59 | By default, it is used a **high** edit level that allows you to add new nodes, 60 | reset node values to null, completely remove node, change value and type of the node (to object, array, string, number, boolean, function, null), 61 | drag and sort tree nodes. 62 | 63 | If you want to operate only with key-values of the nodes and to avoid transformation of json tree, you can add **low** `edit-level` attribute like: 64 | ```html 65 | 66 | ``` 67 | 68 | You can also customize initial depth of tree view by adding `collapsed-level` attribute like, that takes a numeric value: 69 | ```html 70 | 71 | ``` 72 | If `collapsed-level` <= 0, then json-tree is fully collapsed. If `collapsed-level` == 1, then the first level node would be uncollapsed. 73 | If == 2 - the first and second level nodes. And so on. 74 | 75 | You can completely refresh directive by using directive internal refresh function. To access this function just add `node` attribute like: 76 | ```html 77 | 78 | ``` 79 | and then use it in controller as: 80 | ```javascript 81 | $scope.nodeOptions.refresh(); 82 | ``` 83 | 84 | Drag and sort your tree nodes via pressed `Ctrl` key. 85 | 86 | Add more style to prettify the view. See complete example in `example.html` file. 87 | 88 | ### Custom template 89 | 90 | The default template can be overridden by new custom template as follows: 91 | ```js 92 | angular.module('myApp', ['json-tree']) 93 | 94 | .controller('myCtrl', ['$scope', 'jsonTreeConfig', function($scope, jsonTreeConfig){ 95 | jsonTreeConfig.templateUrl = 'custom-template.html'; 96 | }]); 97 | ``` 98 | 99 | --- 100 | For more details of technically usage, please, watch example [online](http://krispo.github.io/json-tree) and test it. 101 | There is given a short instruction. 102 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-tree", 3 | "version": "0.1.5", 4 | "description": "An AngularJS directive for displaying and editing JSON tree", 5 | "main": ["json-tree.js", "json-tree.css"], 6 | "license": "MIT", 7 | "keywords": [ 8 | "json", 9 | "tree", 10 | "json-tree", 11 | "angular", 12 | "directive", 13 | "nested", 14 | "sortable", 15 | "editable" 16 | ], 17 | "authors": [ 18 | "Konstantin Skipor" 19 | ], 20 | "homepage": "http://krispo.github.io/json-tree", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/krispo/json-tree.git" 24 | }, 25 | "dependencies": { 26 | "angular": "^1.x" 27 | }, 28 | "ignore": [ 29 | "**/.*", 30 | "node_modules", 31 | "bower_components", 32 | "src", 33 | "test", 34 | "tests", 35 | "lib", 36 | "examples" 37 | ] 38 | } -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSON Viewer 5 | 6 | 14 | 15 | 16 |

Editable JSON-tree AngularJS directive

17 |
Operations with nodes: add, reset, remove, change value and type, drag and sort
18 |
19 | View on Github 20 |
21 |
22 |

High edit level (default)

23 |

Drag and sort tree nodes via pressed 'Ctrl' key

24 | 25 | 26 | 27 |

28 | 29 | 30 | 31 | 32 | 33 |
{{jsonData | json}}
34 |

35 |
36 | --- is an input field, where you can input any JSON data. The directive automatically determine the type and reconstruct tree view. 37 |
Examples of typing:
38 | [1,2,3] --- convert to Array,
39 | {"json": "tree"} --- convert to Object,
40 | true --- convert to Boolean,
41 | 20.14 --- convert to Number,
42 | Hello World! --- convert to String,
43 | function(){} --- convert to Function 44 |
45 |
46 | « + » --- add element(s) to the collection. For example, if you add [4,"5",6] to [1,2,3] -> the result is [1,2,3,4,"5",6]. If you add [[4,"5",6]] to [1,2,3] -> the result is [1,2,3,[4,"5",6]]. 47 |
48 | « ~ » --- reset node value to null. 49 |
50 | « - » --- completely remove the node. 51 |
52 | «Ctrl» --- press Ctrl key to drag and sort tree nodes. 53 |
54 |
55 |
56 |
57 |

Low edit level (add attributes: edit-level="low", collapsed-level="3")

58 | 59 | 60 | 61 |

62 | 63 | 64 | 65 | 66 | 67 |
{{jsonData | json}}
68 |
69 |
70 | 71 | 72 | 73 | 146 | 147 | -------------------------------------------------------------------------------- /json-tree.css: -------------------------------------------------------------------------------- 1 | /* 2 | JSON-TREE styles 3 | */ 4 | json-tree { 5 | font-size: 13px; 6 | font-family: monospace; 7 | } 8 | json-tree input[type="text"]{ 9 | width: 4em; 10 | height: 12px; 11 | } 12 | json-tree input[type="number"]{ 13 | width: 4em; 14 | height: 12px; 15 | vertical-align: middle; 16 | } 17 | json-tree textarea { 18 | width: 5em; 19 | height: 12px; 20 | padding-top: 0; 21 | margin-bottom: -4px; 22 | border-color: gray; 23 | resize: none; 24 | overflow: hidden; 25 | } 26 | json-tree textarea:focus { 27 | width: auto; 28 | min-height: 100px; 29 | height: auto; 30 | position: absolute; 31 | z-index: 10; 32 | resize: inherit; 33 | overflow: auto; 34 | } 35 | json-tree textarea:focus.valid { 36 | background-color: #d6e9c9; 37 | } 38 | json-tree textarea:focus.invalid { 39 | background-color: rgb(237, 200, 188); 40 | } 41 | json-tree ul { 42 | list-style: none; 43 | margin: 0 0 0 -24px; 44 | } 45 | json-tree .key { 46 | color: darkred; 47 | } 48 | json-tree .add { 49 | color: green; 50 | cursor: pointer; 51 | } 52 | json-tree .reset { 53 | color: steelblue; 54 | cursor: pointer; 55 | } 56 | json-tree .remove { 57 | color: red; 58 | cursor: pointer; 59 | } 60 | json-tree .comma { 61 | color: #c0c0c0; 62 | } 63 | json-tree .drag { 64 | position: absolute; 65 | background-color: #e2efff; 66 | opacity:0.5; 67 | cursor: pointer; 68 | } 69 | json-tree .empty { 70 | border: 1px dashed darkred; 71 | background-color: #e2efff; 72 | } 73 | 74 | json-tree .add-item-block button { 75 | margin-left: 3px; 76 | font-size:11px; 77 | cursor: pointer; 78 | } -------------------------------------------------------------------------------- /json-tree.js: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * JSON-tree, v0.1.5; MIT License 3 | * http://krispo.github.io/json-tree 4 | **************************************************************************/ 5 | (function(){ 6 | 7 | 'use strict'; 8 | 9 | angular.module('json-tree', []) 10 | .constant('jsonTreeConfig', { 11 | templateUrl: null 12 | }) 13 | 14 | .directive('jsonTree', ['$compile', '$q', '$http', '$templateCache', 'jsonTreeConfig', function($compile, $q, $http, $templateCache, jsonTreeConfig) { 15 | 16 | var template = 17 | '' + 18 | '' + 19 | '' + 36 | '' + 37 | ' + ' + 38 | '' + 39 | ''+ 40 | ': '+ 41 | ''+ 42 | '' + 43 | ''+ 44 | ''+ 45 | '' + 46 | ''+ 47 | '' + 48 | ''; 49 | 50 | function getTemplatePromise() { 51 | if(jsonTreeConfig.templateUrl) return $http.get(jsonTreeConfig.templateUrl, { 52 | cache: $templateCache 53 | }).then(function (result) { 54 | return result.data; 55 | }); 56 | 57 | return $q.when(template); 58 | } 59 | 60 | return { 61 | restrict: 'EA', 62 | scope: { 63 | json: '=', 64 | node: '=?', 65 | childs: '=?', 66 | editLevel: '@', 67 | collapsedLevel: '@', 68 | timeout: '@', 69 | timeoutInit: '@' 70 | }, 71 | controller: ['$scope', function($scope){ 72 | 73 | /* initialize container for child nodes */ 74 | $scope.childs = {}; 75 | 76 | /* initialize container for nodes with functions */ 77 | $scope.jsonFn = {}; 78 | 79 | /* define auxiliary functions */ 80 | $scope.utils = { 81 | 82 | /* prettify json view */ 83 | wrap: { 84 | start: function(node){ 85 | if (node === undefined || node === null) return ''; 86 | switch (node.type()){ 87 | case 'array': return '['; 88 | case 'object': return '{'; 89 | default: return ''; 90 | } 91 | }, 92 | middle: function(node){ 93 | if (node === undefined || node === null) return ''; 94 | switch (node.type()){ 95 | case 'array': return '...'; 96 | case 'object': return '...'; 97 | default: return ''; 98 | } 99 | }, 100 | end: function(node){ 101 | if (node === undefined || node === null) return ''; 102 | switch (node.type()){ 103 | case 'array': return ']'; 104 | case 'object': return '}'; 105 | default: return ''; 106 | } 107 | }, 108 | isLastIndex: function(node, index){ 109 | if (node === undefined || node === null) return true; 110 | else return index >= node.length(); 111 | } 112 | }, 113 | 114 | /* collapse/expand node by clicking */ 115 | clickNode: function(node){ 116 | node.isCollapsed = !node.isCollapsed; 117 | }, 118 | 119 | /* add new node to the collection */ 120 | addNode: function(key, value){ 121 | var json = null; 122 | try { json = JSON.parse(value); } catch (e){} //try get json 123 | if (json === null) json = $scope.utils.tryGetFunction(value) || json; //try get function 124 | 125 | /* add element to the object */ 126 | if ($scope.node.type() === 'object') { 127 | if (json !== null){ 128 | $scope.json[key] = json 129 | } else { 130 | $scope.json[key] = value 131 | } 132 | 133 | } 134 | /* add element(s) to the array */ 135 | else if ($scope.node.type() === 'array') { 136 | if (json !== null) { 137 | if (json.constructor === Array){ 138 | /* push new array elements to the array */ 139 | $scope.json.push.apply($scope.json, json); 140 | } else { 141 | /* push single element to the array */ 142 | $scope.json.push(json); 143 | } 144 | } else { 145 | $scope.json.push(value); 146 | } 147 | } 148 | $scope.refresh(); 149 | }, 150 | 151 | /* reset node value by key to default == null */ 152 | resetNode: function(key){ 153 | $scope.json[key] = null; 154 | $scope.refresh(); 155 | }, 156 | 157 | /* remove node by key from json */ 158 | removeNode: function(key){ 159 | if ($scope.node.type() === 'object') 160 | delete $scope.json[key]; 161 | else if ($scope.node.type() === 'array') 162 | $scope.json.splice(key, 1); 163 | $scope.refresh(); 164 | }, 165 | 166 | /* validate text if input to the form */ 167 | validateNode: function(key){ 168 | /* check if null */ 169 | if ($scope.json[key] === null); 170 | 171 | /* check if undefined or "" */ 172 | else if ($scope.json[key] === undefined || $scope.json[key] === '') $scope.json[key] = null; 173 | 174 | /* try to convert string to number */ 175 | else if (!isNaN(+$scope.json[key]) && isFinite($scope.json[key])) 176 | $scope.json[key] = +$scope.json[key]; 177 | 178 | /* try parse to function */ 179 | else if ($scope.utils.tryGetFunction($scope.json[key])){ 180 | $scope.json[key] = $scope.utils.tryGetFunction($scope.json[key]); 181 | $scope.utils.textarea.init(key); 182 | } 183 | 184 | /* try to parse string to json */ 185 | else { 186 | if ($scope.node.isHighEditLevel){ /* if high editable level */ 187 | try { 188 | var json = JSON.parse($scope.json[key]); 189 | $scope.json[key] = json; 190 | $scope.refresh(); 191 | } catch (e){} 192 | } else { /* if low editable level */ 193 | /* check if boolean input -> then refresh */ 194 | if ($scope.json[key] === "true" || $scope.json[key] === "false") { 195 | $scope.json[key] = JSON.parse($scope.json[key]); 196 | $scope.refresh(); 197 | } 198 | } 199 | } 200 | }, 201 | 202 | /* move node from position with index 'i' to position with index 'j' */ 203 | moveNode: function(i, j){ 204 | /* moving for object */ 205 | if ($scope.node.type() === 'object'){ 206 | var json = {}, 207 | keys = Object.keys($scope.json), 208 | key1 = keys[i], 209 | key2 = keys[j]; 210 | 211 | angular.forEach($scope.json, function(value, key){ 212 | if (key == key2) { 213 | if (j > i){ 214 | json[key2] = $scope.json[key2]; 215 | json[key1] = $scope.json[key1]; 216 | } else { 217 | json[key1] = $scope.json[key1]; 218 | json[key2] = $scope.json[key2]; 219 | } 220 | } 221 | else if (key != key1) json[key] = value; 222 | }); 223 | $scope.json = json; 224 | } 225 | 226 | /* moving for array */ 227 | else if ($scope.node.type() === 'array'){ 228 | var temp = $scope.json[i]; 229 | $scope.json.splice(i, 1); 230 | $scope.json.splice(j, 0, temp); 231 | $scope.$apply($scope.refresh()); 232 | } 233 | }, 234 | 235 | /* handle textarea fith functions */ 236 | textarea: { 237 | /* define function value for textarea */ 238 | init: function(key){ 239 | if ($scope.json[key] !== null) $scope.jsonFn[key] = $scope.json[key].toString().trim(); 240 | }, 241 | 242 | /* validate if element value is function */ 243 | validate: function(key){ 244 | var func = $scope.utils.tryGetFunction($scope.jsonFn[key]); 245 | func 246 | ? angular.element($scope.utils.textarea.element).removeClass('invalid').addClass('valid') 247 | : angular.element($scope.utils.textarea.element).removeClass('valid').addClass('invalid'); 248 | }, 249 | 250 | /* onFocus event handler */ 251 | onFocus: function(e, key){ 252 | $scope.utils.textarea['valueBeforeEditing'] = angular.copy($scope.jsonFn[key]); //keep value before editing 253 | $scope.utils.textarea['element'] = e.currentTarget; 254 | $scope.utils.textarea.validate(key); 255 | }, 256 | 257 | /* onChange event handler */ 258 | onChange: function(key){ 259 | $scope.utils.textarea.validate(key); 260 | }, 261 | 262 | /* onBlur event handler */ 263 | onBlur: function(key){ 264 | //handle only if the field has been changed 265 | if ($scope.utils.textarea.valueBeforeEditing !== $scope.jsonFn[key]) { 266 | $scope.$emit('onFunctionChanged'); //emit onFunctionChange event if the function definition was changed. 267 | 268 | var func = $scope.utils.tryGetFunction($scope.jsonFn[key]); 269 | if (func) $scope.json[key] = func; 270 | else { //if value is not a valid function 271 | $scope.json[key] = $scope.jsonFn[key]; 272 | delete $scope.jsonFn[key]; 273 | $scope.utils.validateNode(key); //full validation for node 274 | } 275 | } 276 | } 277 | }, 278 | 279 | /* try to convert string to function */ 280 | /* it is important that function element MUST start with 'function' keyword */ 281 | tryGetFunction: function(str){ 282 | if (str.trim().substring(0, 8) === 'function'){ 283 | try { 284 | var func = eval( '(' + str.trim() + ')' ); 285 | return func; 286 | } catch(e){} 287 | } 288 | }, 289 | 290 | /* to skip ordering in ng-repeat */ 291 | keys: function(obj){ 292 | return (obj instanceof Object) ? Object.keys(obj) : []; 293 | }, 294 | 295 | /* get type for variable val */ 296 | getType: function(val){ 297 | if (val === null) return 'null'; 298 | else if (val === undefined) return 'undefined'; 299 | else if (val.constructor === Array) return 'array'; 300 | else if (val.constructor === Object) return 'object'; 301 | else if (val.constructor === String) return 'string'; 302 | else if (val.constructor === Number) return 'number'; 303 | else if (val.constructor === Boolean) return 'boolean'; 304 | else if (val.constructor === Function) return 'function'; 305 | else return 'object' 306 | } 307 | }; 308 | 309 | /* define properties of the current node */ 310 | $scope.node = { 311 | 312 | /* check node is collapsed */ 313 | isCollapsed: ($scope.collapsedLevel && +$scope.collapsedLevel) ? (+$scope.collapsedLevel <= 0) : true, /* set up isCollapsed properties, by default - true */ 314 | 315 | /* check editing level is high */ 316 | isHighEditLevel: $scope.editLevel !== "low", 317 | 318 | /* if childs[key] is dragging now, dragChildKey matches to key */ 319 | dragChildKey: null, 320 | 321 | /* used to get info such as coordinates (top, left, height, width, meanY) of draggable elements by key */ 322 | dragElements: {}, 323 | 324 | /* check current node is object or array */ 325 | isObject: function(){ 326 | return angular.isObject($scope.json) 327 | }, 328 | 329 | /* get type for current node */ 330 | type: function(){ 331 | return $scope.utils.getType($scope.json); 332 | }, 333 | 334 | /* calculate collection length for object or array */ 335 | length: function(){ 336 | return ($scope.json instanceof Object) ? (Object.keys($scope.json).length) : 1 337 | }, 338 | 339 | /* refresh template view */ 340 | refresh: function(){ 341 | $scope.refresh(); 342 | } 343 | }; 344 | }], 345 | link: function(scope, element, attrs){ 346 | 347 | /* define child scope and template */ 348 | var childScope = scope.$new(), 349 | templatePromise = getTemplatePromise(); 350 | 351 | /* define build template function */ 352 | scope.build = function(_scope){ 353 | if (scope.node.isObject()){ 354 | templatePromise.then(function(tpl) { 355 | element.html('').append($compile(tpl)(_scope)); 356 | }); 357 | } 358 | }; 359 | 360 | /* define refresh function */ 361 | scope.refresh = function(){ 362 | childScope.$destroy(); 363 | childScope = scope.$new(); 364 | scope.build(childScope); 365 | }; 366 | 367 | // build template view 368 | if (scope.timeoutInit) { 369 | setTimeout(function(){ 370 | scope.build(childScope); 371 | },scope.timeoutInit); 372 | } else if (scope.timeout && +scope.timeout>=0) { 373 | setTimeout(function(){ 374 | scope.build(childScope); 375 | },scope.timeout); 376 | } else { 377 | scope.build(childScope); 378 | } 379 | } 380 | } 381 | }]) 382 | 383 | .directive('draggable', ['$document', function($document) { 384 | return { 385 | link: function(scope, element, attr) { 386 | var startX, startY, deltaX, deltaY, emptyElement, keys, index; 387 | 388 | /* Save information of the current draggable element to the parent json-tree scope. 389 | * This would be done under initialization */ 390 | scope.node.dragElements[scope.key] = function(){ 391 | return element; 392 | }; 393 | 394 | element.on('mousedown', function(event) { 395 | /* Check if pressed Ctrl or Shift */ 396 | if (event.ctrlKey || event.shiftKey) { 397 | 398 | scope.node.dragChildKey = scope.key; // tell parent scope what child element is draggable now 399 | 400 | var rect = getRectangle(scope.node.dragElements[scope.key]()[0]); 401 | 402 | /* If child element is not draggable, than make the current element draggable */ 403 | if (scope.childs[scope.key].dragChildKey == null) { 404 | // Prevent default dragging of selected content 405 | event.preventDefault(); 406 | 407 | startX = rect.left; 408 | startY = rect.top; 409 | deltaX = event.pageX - startX; 410 | deltaY = event.pageY - startY; 411 | 412 | /* Draggable element should have 'absolute' position style parameter */ 413 | element.addClass('drag'); 414 | element.css({ 415 | width: rect.width + 'px' 416 | }); 417 | setPosition(startX, startY); 418 | 419 | /* Add an empty element to fill the hole */ 420 | emptyElement = angular.element("
"); 421 | emptyElement.css({ 422 | height: (rect.height - 2) + 'px', 423 | width: (rect.width - 2) + 'px' 424 | }); 425 | element.after(emptyElement); 426 | 427 | /* Auxiliary array of json keys to retain the order of the current key's positions */ 428 | keys = Object.keys(scope.json); 429 | index = scope.$index; 430 | 431 | /* Subscribe on document mouse events */ 432 | $document.on('mousemove', mousemoveEventHandler); 433 | $document.on('mouseup', mouseupEventHandler); 434 | } 435 | } 436 | }); 437 | 438 | element.on('mouseup', function(event){ 439 | /* tell parent scope that the current element with his children are now not draggable */ 440 | scope.node.dragChildKey = null; 441 | }); 442 | 443 | function mousemoveEventHandler(event) { 444 | var rect = getRectangle(scope.node.dragElements[scope.key]()[0]), 445 | meanBefore, meanAfter; 446 | 447 | if (index >= keys.length - 1) meanAfter = Infinity; 448 | else meanAfter = getRectangle(scope.node.dragElements[keys[index + 1]]()[0]).meanY; 449 | 450 | if (index <= 0) meanBefore = -Infinity; 451 | else meanBefore = getRectangle(scope.node.dragElements[keys[index - 1]]()[0]).meanY; 452 | 453 | /* Check the criterion for swapping two sibling nodes */ 454 | if (rect.top + rect.height > meanAfter + 1) { 455 | swapKeys(index, index + 1); 456 | scope.node.dragElements[keys[index]]().parent().append(emptyElement); 457 | index += 1; 458 | } 459 | else if (rect.top < meanBefore - 1){ 460 | swapKeys(index, index - 1); 461 | scope.node.dragElements[keys[index]]().parent().prepend(emptyElement); 462 | index -= 1; 463 | } 464 | 465 | setPosition(startX, event.pageY - deltaY) 466 | } 467 | 468 | function mouseupEventHandler() { 469 | /* Fix position and update json and tree view */ 470 | scope.utils.moveNode(scope.$index, index); 471 | scope.$apply(); 472 | 473 | element.removeClass('drag'); 474 | setPosition(startX, startY); 475 | 476 | emptyElement.remove(); 477 | 478 | $document.unbind('mousemove', mousemoveEventHandler); 479 | $document.unbind('mouseup', mouseupEventHandler); 480 | } 481 | 482 | function setPosition(x, y){ 483 | element.css({ 484 | top: y + 'px', 485 | left: x + 'px' 486 | }); 487 | } 488 | 489 | function swapKeys(i, j){ 490 | var key = keys[i]; 491 | keys[i] = keys[j]; 492 | keys[j] = key; 493 | } 494 | 495 | /* Get coordinates of rectangle region for the element 'el' */ 496 | function getRectangle(el){ 497 | var box = el.getBoundingClientRect(), 498 | top = Math.round(box.top + window.pageYOffset), 499 | left = Math.round(box.left + window.pageXOffset), 500 | height = typeof el.offsetHeight === 'undefined' ? 0 : el.offsetHeight, 501 | width = typeof el.offsetWidth === 'undefined' ? 0 : el.offsetWidth; 502 | 503 | return { top: top, left: left, height: height, width: width, meanY: top + height / 2} 504 | } 505 | } 506 | } 507 | }]); 508 | })(); 509 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-tree2", 3 | "version": "0.1.5", 4 | "description": "An AngularJS directive for displaying and editing JSON tree", 5 | "main": "json-tree.js", 6 | "license": "MIT", 7 | "keywords": [ 8 | "json", 9 | "tree", 10 | "json-tree", 11 | "angular", 12 | "directive", 13 | "nested", 14 | "sortable", 15 | "editable" 16 | ], 17 | "author": { 18 | "name": "Konstantin Skipor" 19 | }, 20 | "homepage": "http://krispo.github.io/json-tree", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/krispo/json-tree.git" 24 | }, 25 | "dependencies": { 26 | "angular": "^1.x" 27 | } 28 | } 29 | --------------------------------------------------------------------------------