├── test ├── mocha.opts └── test.js ├── types ├── tslint.json ├── tsconfig.json ├── index.d.ts └── tree-model-tests.ts ├── .travis.yml ├── .eslintrc.json ├── .jshintrc ├── LICENSE ├── .gitignore ├── package.json ├── .jscsrc ├── README.md ├── dist ├── TreeModel-min.js └── TreeModel.js └── index.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --bail 3 | --check-leaks 4 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules":{ 4 | "no-implicit-dependencies": false 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - npm install -g npm 4 | node_js: 5 | - "node" 6 | - "lts/*" 7 | after_success: 8 | - npm run cov 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 2 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "immed": true, 7 | "indent": 2, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "nonew": true, 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "trailing": true, 17 | "maxlen": 120, 18 | "browser": true, 19 | "devel": true, 20 | "node": true, 21 | "white": true, 22 | "onevar": true, 23 | "globals": { 24 | "require": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es6" 6 | ], 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noEmit": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "tree-model": [ 16 | "." 17 | ] 18 | } 19 | }, 20 | "files": [ 21 | "index.d.ts", 22 | "tree-model-tests.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 João Nuno Silva 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-model", 3 | "version": "1.0.7", 4 | "description": "Manipulate and traverse tree-like structures in javascript.", 5 | "keywords": [ 6 | "tree", 7 | "hierarchy", 8 | "browser", 9 | "node", 10 | "requirejs" 11 | ], 12 | "homepage": "http://jnuno.com/tree-model-js", 13 | "repository": "https://github.com/joaonuno/tree-model-js", 14 | "bugs": "https://github.com/joaonuno/tree-model-js/issues", 15 | "license": "MIT", 16 | "author": "João Nuno Silva (http://jnuno.com)", 17 | "main": "index.js", 18 | "scripts": { 19 | "test": "istanbul cover _mocha", 20 | "cov": "cat ./coverage/lcov.info | coveralls", 21 | "lint": "jshint index.js test/test.js && eslint index.js test/test.js && npm run dtslint", 22 | "preversion": "npm test && npm run lint && npm run dist && git add dist/TreeModel*.js", 23 | "postversion": "git push && git push --tags && echo 'Dont forget to publish to npm...'", 24 | "dist": "rm -rf dist && mkdir -p dist && browserify index.js -o dist/TreeModel.js -s TreeModel && uglifyjs dist/TreeModel.js > dist/TreeModel-min.js", 25 | "dtslint": "dtslint types" 26 | }, 27 | "devDependencies": { 28 | "browserify": "^16", 29 | "chai": "^4", 30 | "coveralls": "^3", 31 | "dtslint": "0.3.0", 32 | "eslint": "^4", 33 | "istanbul": "^0", 34 | "jshint": "^2", 35 | "mocha": "^5", 36 | "sinon": "^5", 37 | "uglify-js": "^3" 38 | }, 39 | "dependencies": { 40 | "mergesort": "0.0.1", 41 | "find-insert-index": "0.0.1" 42 | }, 43 | "types": "types" 44 | } 45 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Project: https://github.com/joaonuno/tree-model-js 2 | // Definitions by: Abhas Bhattacharya 3 | // TypeScript Version: 2.2 4 | 5 | export = TreeModel; 6 | 7 | declare class TreeModel { 8 | constructor(config?: TreeModel.Config); 9 | 10 | private config: TreeModel.Config; 11 | 12 | parse(model: TreeModel.Model): TreeModel.Node; 13 | } 14 | 15 | declare namespace TreeModel { 16 | class Node { 17 | constructor(config: any, model: Model); 18 | 19 | isRoot(): boolean; 20 | hasChildren(): boolean; 21 | addChild(child: Node): Node; 22 | addChildAtIndex(child: Node, index: number): Node; 23 | setIndex(index: number): Node; 24 | getPath(): Array>; 25 | getIndex(): number; 26 | 27 | walk(options: Options, fn: NodeVisitorFunction, ctx?: object): void; 28 | walk(fn: NodeVisitorFunction, ctx?: object): void; 29 | 30 | all(options: Options, fn: NodeVisitorFunction, ctx?: object): Array>; 31 | all(fn: NodeVisitorFunction, ctx?: object): Array>; 32 | 33 | first(options: Options, fn: NodeVisitorFunction, ctx?: object): Node | undefined; 34 | first(fn: NodeVisitorFunction, ctx?: object): Node | undefined; 35 | 36 | drop(): Node; 37 | 38 | [propName: string]: any; 39 | } 40 | 41 | interface Config { 42 | /** 43 | * The name for the children array property. Default is "children". 44 | */ 45 | childrenPropertyName?: string; 46 | modelComparatorFn?: ComparatorFunction; 47 | [propName: string]: any; 48 | } 49 | 50 | interface Options { 51 | strategy: StrategyName; 52 | } 53 | 54 | type StrategyName = "pre" | "post" | "breadth"; 55 | 56 | type ComparatorFunction = (left: any, right: any) => boolean; 57 | type NodeVisitorFunction = (visitingNode: Node) => boolean; 58 | 59 | type Model = T & { children?: Array> }; 60 | } 61 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireSpaceAfterKeywords": [ 12 | "if", 13 | "else", 14 | "for", 15 | "while", 16 | "do", 17 | "switch", 18 | "case", 19 | "return", 20 | "try", 21 | "catch", 22 | "function", 23 | "typeof" 24 | ], 25 | "requireSpaceBeforeBlockStatements": true, 26 | "requireParenthesesAroundIIFE": true, 27 | "requireSpacesInConditionalExpression": true, 28 | "disallowSpacesInNamedFunctionExpression": { 29 | "beforeOpeningRoundBrace": true 30 | }, 31 | "disallowSpacesInFunctionDeclaration": { 32 | "beforeOpeningRoundBrace": true 33 | }, 34 | "requireMultipleVarDecl": "onevar", 35 | "requireBlocksOnNewline": 1, 36 | "disallowEmptyBlocks": true, 37 | "disallowSpacesInsideArrayBrackets": true, 38 | "disallowSpacesInsideParentheses": true, 39 | "disallowQuotedKeysInObjects": true, 40 | "disallowDanglingUnderscores": true, 41 | "disallowSpaceAfterObjectKeys": true, 42 | "requireCommaBeforeLineBreak": true, 43 | "disallowSpaceAfterPrefixUnaryOperators": true, 44 | "disallowSpaceBeforePostfixUnaryOperators": true, 45 | "disallowSpaceBeforeBinaryOperators": [ 46 | ",", ":" 47 | ], 48 | "requireSpaceBeforeBinaryOperators": true, 49 | "requireSpaceAfterBinaryOperators": true, 50 | "requireCamelCaseOrUpperCaseIdentifiers": true, 51 | "disallowKeywords": [ "with" ], 52 | "disallowMultipleLineBreaks": true, 53 | "validateLineBreaks": "LF", 54 | "validateQuoteMarks": "'", 55 | "validateIndentation": 2, 56 | "disallowMixedSpacesAndTabs": true, 57 | "disallowTrailingWhitespace": true, 58 | "disallowTrailingComma": true, 59 | "disallowKeywordsOnNewLine": [ "else" ], 60 | "requireLineFeedAtFileEnd": true, 61 | "requireCapitalizedConstructors": true, 62 | "requireDotNotation": true, 63 | "disallowYodaConditions": true 64 | } -------------------------------------------------------------------------------- /types/tree-model-tests.ts: -------------------------------------------------------------------------------- 1 | import TreeModel = require("tree-model"); 2 | 3 | interface TestModel { 4 | name: string; 5 | } 6 | 7 | const tree = new TreeModel({}); 8 | 9 | const root = tree.parse({ name: 'a', children: [{ name: 'b' }, { name: 'c' }] }); 10 | 11 | // $ExpectType boolean 12 | root.isRoot(); 13 | // $ExpectType boolean 14 | root.hasChildren(); 15 | { 16 | root.first(); // $ExpectError 17 | root.first((node) => node); // $ExpectError 18 | 19 | const tmpNodeB = root.first((node) => node.name === 'b'); 20 | const tmpNodeC = root.first((node) => node.name === 'c'); 21 | if (typeof tmpNodeB !== "undefined" && typeof tmpNodeC !== "undefined") { 22 | const nodeB = tmpNodeB; 23 | const nodeC = tmpNodeC; 24 | 25 | type Node = typeof nodeB; 26 | 27 | root.addChild({}); // $ExpectError 28 | root.addChild(nodeC); // $ExpectType Node 29 | 30 | root.addChildAtIndex(nodeC); // $ExpectError 31 | root.addChildAtIndex(nodeC, 0); // $ExpectType Node 32 | 33 | nodeB.setIndex("first"); // $ExpectError 34 | nodeB.setIndex(0); // $ExpectType Node 35 | const arrPath: Node[] = nodeB.getPath(); 36 | const nodeIndex: number = nodeB.getIndex(); 37 | 38 | const opt = { strategy: <'post' | 'pre'> 'post' }; 39 | const visitorFn = (tm: Node) => true; 40 | const ctxObject = {}; 41 | 42 | // Test Node.walk with different overloads no return type 43 | { 44 | { 45 | nodeB.walk(opt, visitorFn, ctxObject); // $ExpectType void 46 | } 47 | { 48 | nodeB.walk(opt, visitorFn); // $ExpectType void 49 | } 50 | { 51 | nodeB.walk(visitorFn, ctxObject); // $ExpectType void 52 | } 53 | } 54 | 55 | // Test Node.all with different overloads and their return type 56 | { 57 | { 58 | const nodeArr: Node[] = nodeB.all(opt, visitorFn, ctxObject); 59 | } 60 | { 61 | const nodeArr: Node[] = nodeB.all(opt, visitorFn); 62 | } 63 | { 64 | const nodeArr: Node[] = nodeB.all(visitorFn, ctxObject); 65 | } 66 | } 67 | 68 | // Test Node.first with different overloads and their return type 69 | { 70 | { 71 | const nodeReturned: Node | undefined = nodeB.first(opt, visitorFn, ctxObject); 72 | } 73 | { 74 | const nodeReturned: Node | undefined = nodeB.first(opt, visitorFn); 75 | } 76 | { 77 | const nodeReturned: Node | undefined = nodeB.first(visitorFn, ctxObject); 78 | } 79 | } 80 | 81 | nodeC.drop(nodeC); // $ExpectError 82 | nodeC.drop(); // $ExpectType Node 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TreeModel 2 | 3 | Manipulate and traverse tree-like structures in javascript. 4 | 5 | For download and demos, please [visit TreeModel website](http://jnuno.com/tree-model-js). 6 | 7 | [![Build Status](https://travis-ci.org/joaonuno/tree-model-js.svg)](https://travis-ci.org/joaonuno/tree-model-js) 8 | [![Coverage Status](https://coveralls.io/repos/github/joaonuno/tree-model-js/badge.svg?branch=master)](https://coveralls.io/github/joaonuno/tree-model-js?branch=master) 9 | 10 | ## Installation 11 | 12 | ### Node 13 | 14 | TreeModel is available as an npm module so you can install it with `npm install tree-model` and use it in your script: 15 | 16 | ```js 17 | var TreeModel = require('tree-model'), 18 | tree = new TreeModel(), 19 | root = tree.parse({name: 'a', children: [{name: 'b'}]}); 20 | ``` 21 | 22 | #### TypeScript 23 | Type definitions are already bundled with the package, which should just work with npm install. 24 | 25 | You can maually find the definition files in the `types` folder. 26 | 27 | ### Browser 28 | 29 | [Visit TreeModel website](http://jnuno.com/tree-model-js) to download browser-ready bundles. 30 | 31 | ## Questions? 32 | 33 | If you have any doubt using this library please post a question on [stackoverflow](http://stackoverflow.com/questions/ask?tags=treemodel) tagged with `treemodel`. 34 | 35 | ## API Reference 36 | 37 | ### Create a new TreeModel 38 | 39 | Create a new TreeModel with the given options. 40 | 41 | ```js 42 | var tree = new TreeModel(options) 43 | ``` 44 | 45 | Valid properties for the options object are: 46 | 47 | * `childrenPropertyName` - The name for the children array property. Default is `children`; 48 | * `modelComparatorFn` - A comparator function to sort the children when parsing the model and adding children. The default order policy is to keep the parsed order and append new children. The comparator function receives the model for two nodes just like the [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) function. The provided sort algorithm is **stable**. 49 | 50 | ### Parse the hierarchy object 51 | 52 | Parse the given user defined model and return the root Node object. 53 | 54 | ```js 55 | Node tree.parse(model) 56 | ``` 57 | 58 | ### Is Root? 59 | 60 | Return `true` if this Node is the root, `false` otherwise. 61 | 62 | ```js 63 | Boolean node.isRoot() 64 | ``` 65 | 66 | ### Has Children? 67 | 68 | Return `true` if this Node has one or more children, `false` otherwise. 69 | 70 | ```js 71 | Boolean node.hasChildren() 72 | ``` 73 | 74 | ### Add a child 75 | 76 | Add the given node as child of this one. Return the child Node. 77 | 78 | ```js 79 | Node parentNode.addChild(childNode) 80 | ``` 81 | 82 | ### Add a child at a given index 83 | 84 | Add the given node as child of this one at the given index. Return the child Node. 85 | 86 | ```js 87 | Node parentNode.addChildAtIndex(childNode, index) 88 | ``` 89 | 90 | ### Set the index of a node among its siblings 91 | 92 | Sets the index of the node among its siblings to the given value. Return the node itself. 93 | 94 | ```js 95 | Node node.setIndex(index) 96 | ``` 97 | 98 | ### Get the index of a node among its siblings 99 | 100 | Gets the index of the node relative to its siblings. Return the index value. 101 | 102 | ```js 103 | Int node.getIndex() 104 | ``` 105 | 106 | ### Get the node path 107 | 108 | Get the array of Nodes representing the path from the root to this Node (inclusive). 109 | 110 | ```js 111 | Array node.getPath() 112 | ``` 113 | 114 | ### Delete a node from the tree 115 | 116 | Drop the subtree starting at this node. Returns the node itself, which is now a root node. 117 | 118 | ```js 119 | Node node.drop() 120 | ``` 121 | 122 | *Warning* - Dropping a node while walking the tree is not supported. You must first collect the nodes to drop using one of the traversal functions and then drop them. Example: 123 | 124 | ```js 125 | root.all( /* predicate */ ).forEach(function (node) { 126 | node.drop(); 127 | }); 128 | ``` 129 | 130 | ### Find a node 131 | 132 | Starting from this node, find the first Node that matches the predicate and return it. The **predicate** is a function wich receives the visited Node and returns `true` if the Node should be picked and `false` otherwise. 133 | 134 | ```js 135 | Node node.first(predicate) 136 | ``` 137 | 138 | ### Find all nodes 139 | 140 | Starting from this node, find all Nodes that match the predicate and return these. 141 | 142 | ```js 143 | Array node.all(predicate) 144 | ``` 145 | 146 | ### Walk the tree 147 | 148 | Starting from this node, traverse the subtree calling the action for each visited node. The action is a function which receives the visited Node as argument. The traversal can be halted by returning `false` from the action. 149 | 150 | ```js 151 | node.walk([options], action, [context]) 152 | ``` 153 | 154 | **Note** - `first`, `all` and `walk` can optionally receive as first argument an object with traversal options. Currently the only supported option is the traversal `strategy` which can be any of the following: 155 | 156 | * `{strategy: 'pre'}` - Depth-first pre-order *[default]*; 157 | * `{strategy: 'post'}` - Depth-first post-order; 158 | * `{strategy: 'breadth'}` - Breadth-first. 159 | 160 | These functions can also take, as the last parameter, the *context* on which the action will be called. 161 | 162 | ## Contributing 163 | 164 | ### Setup 165 | 166 | Fork this repository and run `npm install` on the project root folder to make sure you have all project dependencies installed. 167 | 168 | ### Code Linting 169 | 170 | Run `npm run lint` 171 | 172 | This will check both source and tests for code correctness and style compliance. 173 | 174 | ### Running Tests 175 | 176 | Run `npm test` 177 | 178 | ### Type definitions 179 | 180 | To modify the type definitions, look inside the `types` folder. 181 | `index.d.ts` contains the definition and `tree-model-tests.ts` contains type tests. 182 | 183 | To verify changes: 184 | 185 | Run `npm run dtslint`. 186 | -------------------------------------------------------------------------------- /dist/TreeModel-min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TreeModel=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i0};function addChild(self,child,insertIndex){var index;if(!(child instanceof Node)){throw new TypeError("Child must be of type Node.")}child.parent=self;if(!(self.model[self.config.childrenPropertyName]instanceof Array)){self.model[self.config.childrenPropertyName]=[]}if(hasComparatorFunction(self)){index=findInsertIndex(self.config.modelComparatorFn,self.model[self.config.childrenPropertyName],child.model);self.model[self.config.childrenPropertyName].splice(index,0,child.model);self.children.splice(index,0,child)}else{if(insertIndex===undefined){self.model[self.config.childrenPropertyName].push(child.model);self.children.push(child)}else{if(insertIndex<0||insertIndex>self.children.length){throw new Error("Invalid index.")}self.model[self.config.childrenPropertyName].splice(insertIndex,0,child.model);self.children.splice(insertIndex,0,child)}}return child}Node.prototype.addChild=function(child){return addChild(this,child)};Node.prototype.addChildAtIndex=function(child,index){if(hasComparatorFunction(this)){throw new Error("Cannot add child at index when using a comparator function.")}return addChild(this,child,index)};Node.prototype.setIndex=function(index){if(hasComparatorFunction(this)){throw new Error("Cannot set node index when using a comparator function.")}if(this.isRoot()){if(index===0){return this}throw new Error("Invalid index.")}if(index<0||index>=this.parent.children.length){throw new Error("Invalid index.")}var oldIndex=this.parent.children.indexOf(this);this.parent.children.splice(index,0,this.parent.children.splice(oldIndex,1)[0]);this.parent.model[this.parent.config.childrenPropertyName].splice(index,0,this.parent.model[this.parent.config.childrenPropertyName].splice(oldIndex,1)[0]);return this};Node.prototype.getPath=function(){var path=[];(function addToPath(node){path.unshift(node);if(!node.isRoot()){addToPath(node.parent)}})(this);return path};Node.prototype.getIndex=function(){if(this.isRoot()){return 0}return this.parent.children.indexOf(this)};function parseArgs(){var args={};if(arguments.length===1){if(typeof arguments[0]==="function"){args.fn=arguments[0]}else{args.options=arguments[0]}}else if(arguments.length===2){if(typeof arguments[0]==="function"){args.fn=arguments[0];args.ctx=arguments[1]}else{args.options=arguments[0];args.fn=arguments[1]}}else{args.options=arguments[0];args.fn=arguments[1];args.ctx=arguments[2]}args.options=args.options||{};if(!args.options.strategy){args.options.strategy="pre"}if(!walkStrategies[args.options.strategy]){throw new Error("Unknown tree walk strategy. Valid strategies are 'pre' [default], 'post' and 'breadth'.")}return args}Node.prototype.walk=function(){var args;args=parseArgs.apply(this,arguments);walkStrategies[args.options.strategy].call(this,args.fn,args.ctx)};walkStrategies.pre=function depthFirstPreOrder(callback,context){var i,childCount,keepGoing;keepGoing=callback.call(context,this);for(i=0,childCount=this.children.length;i0){break}}return i}return findInsertIndex}()},{}],3:[function(require,module,exports){module.exports=function(){"use strict";function mergeSort(comparatorFn,arr){var len=arr.length,firstHalf,secondHalf;if(len>=2){firstHalf=arr.slice(0,len/2);secondHalf=arr.slice(len/2,len);return merge(comparatorFn,mergeSort(comparatorFn,firstHalf),mergeSort(comparatorFn,secondHalf))}else{return arr.slice()}}function merge(comparatorFn,arr1,arr2){var result=[],left1=arr1.length,left2=arr2.length;while(left1>0&&left2>0){if(comparatorFn(arr1[0],arr2[0])<=0){result.push(arr1.shift());left1--}else{result.push(arr2.shift());left2--}}if(left1>0){result.push.apply(result,arr1)}else{result.push.apply(result,arr2)}return result}return mergeSort}()},{}]},{},[1])(1)}); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var mergeSort, findInsertIndex; 2 | mergeSort = require('mergesort'); 3 | findInsertIndex = require('find-insert-index'); 4 | 5 | module.exports = (function () { 6 | 'use strict'; 7 | 8 | var walkStrategies; 9 | 10 | walkStrategies = {}; 11 | 12 | function k(result) { 13 | return function () { 14 | return result; 15 | }; 16 | } 17 | 18 | function TreeModel(config) { 19 | config = config || {}; 20 | this.config = config; 21 | this.config.childrenPropertyName = config.childrenPropertyName || 'children'; 22 | this.config.modelComparatorFn = config.modelComparatorFn; 23 | } 24 | 25 | function addChildToNode(node, child) { 26 | child.parent = node; 27 | node.children.push(child); 28 | return child; 29 | } 30 | 31 | function Node(config, model) { 32 | this.config = config; 33 | this.model = model; 34 | this.children = []; 35 | } 36 | 37 | TreeModel.prototype.parse = function (model) { 38 | var i, childCount, node; 39 | 40 | if (!(model instanceof Object)) { 41 | throw new TypeError('Model must be of type object.'); 42 | } 43 | 44 | node = new Node(this.config, model); 45 | if (model[this.config.childrenPropertyName] instanceof Array) { 46 | if (this.config.modelComparatorFn) { 47 | model[this.config.childrenPropertyName] = mergeSort( 48 | this.config.modelComparatorFn, 49 | model[this.config.childrenPropertyName]); 50 | } 51 | for (i = 0, childCount = model[this.config.childrenPropertyName].length; i < childCount; i++) { 52 | addChildToNode(node, this.parse(model[this.config.childrenPropertyName][i])); 53 | } 54 | } 55 | return node; 56 | }; 57 | 58 | function hasComparatorFunction(node) { 59 | return typeof node.config.modelComparatorFn === 'function'; 60 | } 61 | 62 | Node.prototype.isRoot = function () { 63 | return this.parent === undefined; 64 | }; 65 | 66 | Node.prototype.hasChildren = function () { 67 | return this.children.length > 0; 68 | }; 69 | 70 | function addChild(self, child, insertIndex) { 71 | var index; 72 | 73 | if (!(child instanceof Node)) { 74 | throw new TypeError('Child must be of type Node.'); 75 | } 76 | 77 | child.parent = self; 78 | if (!(self.model[self.config.childrenPropertyName] instanceof Array)) { 79 | self.model[self.config.childrenPropertyName] = []; 80 | } 81 | 82 | if (hasComparatorFunction(self)) { 83 | // Find the index to insert the child 84 | index = findInsertIndex( 85 | self.config.modelComparatorFn, 86 | self.model[self.config.childrenPropertyName], 87 | child.model); 88 | 89 | // Add to the model children 90 | self.model[self.config.childrenPropertyName].splice(index, 0, child.model); 91 | 92 | // Add to the node children 93 | self.children.splice(index, 0, child); 94 | } else { 95 | if (insertIndex === undefined) { 96 | self.model[self.config.childrenPropertyName].push(child.model); 97 | self.children.push(child); 98 | } else { 99 | if (insertIndex < 0 || insertIndex > self.children.length) { 100 | throw new Error('Invalid index.'); 101 | } 102 | self.model[self.config.childrenPropertyName].splice(insertIndex, 0, child.model); 103 | self.children.splice(insertIndex, 0, child); 104 | } 105 | } 106 | return child; 107 | } 108 | 109 | Node.prototype.addChild = function (child) { 110 | return addChild(this, child); 111 | }; 112 | 113 | Node.prototype.addChildAtIndex = function (child, index) { 114 | if (hasComparatorFunction(this)) { 115 | throw new Error('Cannot add child at index when using a comparator function.'); 116 | } 117 | 118 | return addChild(this, child, index); 119 | }; 120 | 121 | Node.prototype.setIndex = function (index) { 122 | if (hasComparatorFunction(this)) { 123 | throw new Error('Cannot set node index when using a comparator function.'); 124 | } 125 | 126 | if (this.isRoot()) { 127 | if (index === 0) { 128 | return this; 129 | } 130 | throw new Error('Invalid index.'); 131 | } 132 | 133 | if (index < 0 || index >= this.parent.children.length) { 134 | throw new Error('Invalid index.'); 135 | } 136 | 137 | var oldIndex = this.parent.children.indexOf(this); 138 | 139 | this.parent.children.splice(index, 0, this.parent.children.splice(oldIndex, 1)[0]); 140 | 141 | this.parent.model[this.parent.config.childrenPropertyName] 142 | .splice(index, 0, this.parent.model[this.parent.config.childrenPropertyName].splice(oldIndex, 1)[0]); 143 | 144 | return this; 145 | }; 146 | 147 | Node.prototype.getPath = function () { 148 | var path = []; 149 | (function addToPath(node) { 150 | path.unshift(node); 151 | if (!node.isRoot()) { 152 | addToPath(node.parent); 153 | } 154 | })(this); 155 | return path; 156 | }; 157 | 158 | Node.prototype.getIndex = function () { 159 | if (this.isRoot()) { 160 | return 0; 161 | } 162 | return this.parent.children.indexOf(this); 163 | }; 164 | 165 | /** 166 | * Parse the arguments of traversal functions. These functions can take one optional 167 | * first argument which is an options object. If present, this object will be stored 168 | * in args.options. The only mandatory argument is the callback function which can 169 | * appear in the first or second position (if an options object is given). This 170 | * function will be saved to args.fn. The last optional argument is the context on 171 | * which the callback function will be called. It will be available in args.ctx. 172 | * 173 | * @returns Parsed arguments. 174 | */ 175 | function parseArgs() { 176 | var args = {}; 177 | if (arguments.length === 1) { 178 | if (typeof arguments[0] === 'function') { 179 | args.fn = arguments[0]; 180 | } else { 181 | args.options = arguments[0]; 182 | } 183 | } else if (arguments.length === 2) { 184 | if (typeof arguments[0] === 'function') { 185 | args.fn = arguments[0]; 186 | args.ctx = arguments[1]; 187 | } else { 188 | args.options = arguments[0]; 189 | args.fn = arguments[1]; 190 | } 191 | } else { 192 | args.options = arguments[0]; 193 | args.fn = arguments[1]; 194 | args.ctx = arguments[2]; 195 | } 196 | args.options = args.options || {}; 197 | if (!args.options.strategy) { 198 | args.options.strategy = 'pre'; 199 | } 200 | if (!walkStrategies[args.options.strategy]) { 201 | throw new Error('Unknown tree walk strategy. Valid strategies are \'pre\' [default], \'post\' and \'breadth\'.'); 202 | } 203 | return args; 204 | } 205 | 206 | Node.prototype.walk = function () { 207 | var args; 208 | args = parseArgs.apply(this, arguments); 209 | walkStrategies[args.options.strategy].call(this, args.fn, args.ctx); 210 | }; 211 | 212 | walkStrategies.pre = function depthFirstPreOrder(callback, context) { 213 | var i, childCount, keepGoing; 214 | keepGoing = callback.call(context, this); 215 | for (i = 0, childCount = this.children.length; i < childCount; i++) { 216 | if (keepGoing === false) { 217 | return false; 218 | } 219 | keepGoing = depthFirstPreOrder.call(this.children[i], callback, context); 220 | } 221 | return keepGoing; 222 | }; 223 | 224 | walkStrategies.post = function depthFirstPostOrder(callback, context) { 225 | var i, childCount, keepGoing; 226 | for (i = 0, childCount = this.children.length; i < childCount; i++) { 227 | keepGoing = depthFirstPostOrder.call(this.children[i], callback, context); 228 | if (keepGoing === false) { 229 | return false; 230 | } 231 | } 232 | keepGoing = callback.call(context, this); 233 | return keepGoing; 234 | }; 235 | 236 | walkStrategies.breadth = function breadthFirst(callback, context) { 237 | var queue = [this]; 238 | (function processQueue() { 239 | var i, childCount, node; 240 | if (queue.length === 0) { 241 | return; 242 | } 243 | node = queue.shift(); 244 | for (i = 0, childCount = node.children.length; i < childCount; i++) { 245 | queue.push(node.children[i]); 246 | } 247 | if (callback.call(context, node) !== false) { 248 | processQueue(); 249 | } 250 | })(); 251 | }; 252 | 253 | Node.prototype.all = function () { 254 | var args, all = []; 255 | args = parseArgs.apply(this, arguments); 256 | args.fn = args.fn || k(true); 257 | walkStrategies[args.options.strategy].call(this, function (node) { 258 | if (args.fn.call(args.ctx, node)) { 259 | all.push(node); 260 | } 261 | }, args.ctx); 262 | return all; 263 | }; 264 | 265 | Node.prototype.first = function () { 266 | var args, first; 267 | args = parseArgs.apply(this, arguments); 268 | args.fn = args.fn || k(true); 269 | walkStrategies[args.options.strategy].call(this, function (node) { 270 | if (args.fn.call(args.ctx, node)) { 271 | first = node; 272 | return false; 273 | } 274 | }, args.ctx); 275 | return first; 276 | }; 277 | 278 | Node.prototype.drop = function () { 279 | var indexOfChild; 280 | if (!this.isRoot()) { 281 | indexOfChild = this.parent.children.indexOf(this); 282 | this.parent.children.splice(indexOfChild, 1); 283 | this.parent.model[this.config.childrenPropertyName].splice(indexOfChild, 1); 284 | this.parent = undefined; 285 | delete this.parent; 286 | } 287 | return this; 288 | }; 289 | 290 | return TreeModel; 291 | })(); 292 | -------------------------------------------------------------------------------- /dist/TreeModel.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TreeModel = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0; 69 | }; 70 | 71 | function addChild(self, child, insertIndex) { 72 | var index; 73 | 74 | if (!(child instanceof Node)) { 75 | throw new TypeError('Child must be of type Node.'); 76 | } 77 | 78 | child.parent = self; 79 | if (!(self.model[self.config.childrenPropertyName] instanceof Array)) { 80 | self.model[self.config.childrenPropertyName] = []; 81 | } 82 | 83 | if (hasComparatorFunction(self)) { 84 | // Find the index to insert the child 85 | index = findInsertIndex( 86 | self.config.modelComparatorFn, 87 | self.model[self.config.childrenPropertyName], 88 | child.model); 89 | 90 | // Add to the model children 91 | self.model[self.config.childrenPropertyName].splice(index, 0, child.model); 92 | 93 | // Add to the node children 94 | self.children.splice(index, 0, child); 95 | } else { 96 | if (insertIndex === undefined) { 97 | self.model[self.config.childrenPropertyName].push(child.model); 98 | self.children.push(child); 99 | } else { 100 | if (insertIndex < 0 || insertIndex > self.children.length) { 101 | throw new Error('Invalid index.'); 102 | } 103 | self.model[self.config.childrenPropertyName].splice(insertIndex, 0, child.model); 104 | self.children.splice(insertIndex, 0, child); 105 | } 106 | } 107 | return child; 108 | } 109 | 110 | Node.prototype.addChild = function (child) { 111 | return addChild(this, child); 112 | }; 113 | 114 | Node.prototype.addChildAtIndex = function (child, index) { 115 | if (hasComparatorFunction(this)) { 116 | throw new Error('Cannot add child at index when using a comparator function.'); 117 | } 118 | 119 | return addChild(this, child, index); 120 | }; 121 | 122 | Node.prototype.setIndex = function (index) { 123 | if (hasComparatorFunction(this)) { 124 | throw new Error('Cannot set node index when using a comparator function.'); 125 | } 126 | 127 | if (this.isRoot()) { 128 | if (index === 0) { 129 | return this; 130 | } 131 | throw new Error('Invalid index.'); 132 | } 133 | 134 | if (index < 0 || index >= this.parent.children.length) { 135 | throw new Error('Invalid index.'); 136 | } 137 | 138 | var oldIndex = this.parent.children.indexOf(this); 139 | 140 | this.parent.children.splice(index, 0, this.parent.children.splice(oldIndex, 1)[0]); 141 | 142 | this.parent.model[this.parent.config.childrenPropertyName] 143 | .splice(index, 0, this.parent.model[this.parent.config.childrenPropertyName].splice(oldIndex, 1)[0]); 144 | 145 | return this; 146 | }; 147 | 148 | Node.prototype.getPath = function () { 149 | var path = []; 150 | (function addToPath(node) { 151 | path.unshift(node); 152 | if (!node.isRoot()) { 153 | addToPath(node.parent); 154 | } 155 | })(this); 156 | return path; 157 | }; 158 | 159 | Node.prototype.getIndex = function () { 160 | if (this.isRoot()) { 161 | return 0; 162 | } 163 | return this.parent.children.indexOf(this); 164 | }; 165 | 166 | /** 167 | * Parse the arguments of traversal functions. These functions can take one optional 168 | * first argument which is an options object. If present, this object will be stored 169 | * in args.options. The only mandatory argument is the callback function which can 170 | * appear in the first or second position (if an options object is given). This 171 | * function will be saved to args.fn. The last optional argument is the context on 172 | * which the callback function will be called. It will be available in args.ctx. 173 | * 174 | * @returns Parsed arguments. 175 | */ 176 | function parseArgs() { 177 | var args = {}; 178 | if (arguments.length === 1) { 179 | if (typeof arguments[0] === 'function') { 180 | args.fn = arguments[0]; 181 | } else { 182 | args.options = arguments[0]; 183 | } 184 | } else if (arguments.length === 2) { 185 | if (typeof arguments[0] === 'function') { 186 | args.fn = arguments[0]; 187 | args.ctx = arguments[1]; 188 | } else { 189 | args.options = arguments[0]; 190 | args.fn = arguments[1]; 191 | } 192 | } else { 193 | args.options = arguments[0]; 194 | args.fn = arguments[1]; 195 | args.ctx = arguments[2]; 196 | } 197 | args.options = args.options || {}; 198 | if (!args.options.strategy) { 199 | args.options.strategy = 'pre'; 200 | } 201 | if (!walkStrategies[args.options.strategy]) { 202 | throw new Error('Unknown tree walk strategy. Valid strategies are \'pre\' [default], \'post\' and \'breadth\'.'); 203 | } 204 | return args; 205 | } 206 | 207 | Node.prototype.walk = function () { 208 | var args; 209 | args = parseArgs.apply(this, arguments); 210 | walkStrategies[args.options.strategy].call(this, args.fn, args.ctx); 211 | }; 212 | 213 | walkStrategies.pre = function depthFirstPreOrder(callback, context) { 214 | var i, childCount, keepGoing; 215 | keepGoing = callback.call(context, this); 216 | for (i = 0, childCount = this.children.length; i < childCount; i++) { 217 | if (keepGoing === false) { 218 | return false; 219 | } 220 | keepGoing = depthFirstPreOrder.call(this.children[i], callback, context); 221 | } 222 | return keepGoing; 223 | }; 224 | 225 | walkStrategies.post = function depthFirstPostOrder(callback, context) { 226 | var i, childCount, keepGoing; 227 | for (i = 0, childCount = this.children.length; i < childCount; i++) { 228 | keepGoing = depthFirstPostOrder.call(this.children[i], callback, context); 229 | if (keepGoing === false) { 230 | return false; 231 | } 232 | } 233 | keepGoing = callback.call(context, this); 234 | return keepGoing; 235 | }; 236 | 237 | walkStrategies.breadth = function breadthFirst(callback, context) { 238 | var queue = [this]; 239 | (function processQueue() { 240 | var i, childCount, node; 241 | if (queue.length === 0) { 242 | return; 243 | } 244 | node = queue.shift(); 245 | for (i = 0, childCount = node.children.length; i < childCount; i++) { 246 | queue.push(node.children[i]); 247 | } 248 | if (callback.call(context, node) !== false) { 249 | processQueue(); 250 | } 251 | })(); 252 | }; 253 | 254 | Node.prototype.all = function () { 255 | var args, all = []; 256 | args = parseArgs.apply(this, arguments); 257 | args.fn = args.fn || k(true); 258 | walkStrategies[args.options.strategy].call(this, function (node) { 259 | if (args.fn.call(args.ctx, node)) { 260 | all.push(node); 261 | } 262 | }, args.ctx); 263 | return all; 264 | }; 265 | 266 | Node.prototype.first = function () { 267 | var args, first; 268 | args = parseArgs.apply(this, arguments); 269 | args.fn = args.fn || k(true); 270 | walkStrategies[args.options.strategy].call(this, function (node) { 271 | if (args.fn.call(args.ctx, node)) { 272 | first = node; 273 | return false; 274 | } 275 | }, args.ctx); 276 | return first; 277 | }; 278 | 279 | Node.prototype.drop = function () { 280 | var indexOfChild; 281 | if (!this.isRoot()) { 282 | indexOfChild = this.parent.children.indexOf(this); 283 | this.parent.children.splice(indexOfChild, 1); 284 | this.parent.model[this.config.childrenPropertyName].splice(indexOfChild, 1); 285 | this.parent = undefined; 286 | delete this.parent; 287 | } 288 | return this; 289 | }; 290 | 291 | return TreeModel; 292 | })(); 293 | 294 | },{"find-insert-index":2,"mergesort":3}],2:[function(require,module,exports){ 295 | module.exports = (function () { 296 | 'use strict'; 297 | 298 | /** 299 | * Find the index to insert an element in array keeping the sort order. 300 | * 301 | * @param {function} comparatorFn The comparator function which sorted the array. 302 | * @param {array} arr The sorted array. 303 | * @param {object} el The element to insert. 304 | */ 305 | function findInsertIndex(comparatorFn, arr, el) { 306 | var i, len; 307 | for (i = 0, len = arr.length; i < len; i++) { 308 | if (comparatorFn(arr[i], el) > 0) { 309 | break; 310 | } 311 | } 312 | return i; 313 | } 314 | 315 | return findInsertIndex; 316 | })(); 317 | 318 | },{}],3:[function(require,module,exports){ 319 | module.exports = (function () { 320 | 'use strict'; 321 | 322 | /** 323 | * Sort an array using the merge sort algorithm. 324 | * 325 | * @param {function} comparatorFn The comparator function. 326 | * @param {array} arr The array to sort. 327 | * @returns {array} The sorted array. 328 | */ 329 | function mergeSort(comparatorFn, arr) { 330 | var len = arr.length, firstHalf, secondHalf; 331 | if (len >= 2) { 332 | firstHalf = arr.slice(0, len / 2); 333 | secondHalf = arr.slice(len / 2, len); 334 | return merge(comparatorFn, mergeSort(comparatorFn, firstHalf), mergeSort(comparatorFn, secondHalf)); 335 | } else { 336 | return arr.slice(); 337 | } 338 | } 339 | 340 | /** 341 | * The merge part of the merge sort algorithm. 342 | * 343 | * @param {function} comparatorFn The comparator function. 344 | * @param {array} arr1 The first sorted array. 345 | * @param {array} arr2 The second sorted array. 346 | * @returns {array} The merged and sorted array. 347 | */ 348 | function merge(comparatorFn, arr1, arr2) { 349 | var result = [], left1 = arr1.length, left2 = arr2.length; 350 | while (left1 > 0 && left2 > 0) { 351 | if (comparatorFn(arr1[0], arr2[0]) <= 0) { 352 | result.push(arr1.shift()); 353 | left1--; 354 | } else { 355 | result.push(arr2.shift()); 356 | left2--; 357 | } 358 | } 359 | if (left1 > 0) { 360 | result.push.apply(result, arr1); 361 | } else { 362 | result.push.apply(result, arr2); 363 | } 364 | return result; 365 | } 366 | 367 | return mergeSort; 368 | })(); 369 | 370 | },{}]},{},[1])(1) 371 | }); 372 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 3 | var chai, assert, sinon, TreeModel; 4 | chai = require('chai'); 5 | sinon = require('sinon'); 6 | TreeModel = require('..'); 7 | assert = chai.assert; 8 | chai.config.includeStack = true; 9 | 10 | describe('TreeModel', function () { 11 | 'use strict'; 12 | 13 | function idEq(id) { 14 | return function (node) { 15 | return node.model.id === id; 16 | }; 17 | } 18 | 19 | describe('with default configuration', function () { 20 | var treeModel; 21 | 22 | beforeEach(function () { 23 | treeModel = new TreeModel(); 24 | }); 25 | 26 | describe('parse()', function () { 27 | it('should throw an error when model is a number (not an object)', function () { 28 | assert.throws(treeModel.parse.bind(treeModel, 1), TypeError, 'Model must be of type object.'); 29 | }); 30 | 31 | it('should throw an error when model is a string (not an object)', function () { 32 | assert.throws(treeModel.parse.bind(treeModel, 'string'), TypeError, 'Model must be of type object.'); 33 | }); 34 | 35 | it('should throw an error when some child in the model is not an object', function () { 36 | assert.throws( 37 | treeModel.parse.bind(treeModel, {children: ['string']}), 38 | TypeError, 39 | 'Model must be of type object.'); 40 | }); 41 | 42 | it('should create a root node when given a model without children', function () { 43 | var root; 44 | 45 | root = treeModel.parse({id: 1}); 46 | 47 | assert.isUndefined(root.parent); 48 | assert.isArray(root.children); 49 | assert.lengthOf(root.children, 0); 50 | assert.deepEqual(root.model, {id: 1}); 51 | }); 52 | 53 | it('should create a root and the respective children when given a model with children', function () { 54 | var root, node12; 55 | 56 | root = treeModel.parse({ 57 | id: 1, 58 | children: [ 59 | { 60 | id: 11, 61 | children: [{id: 111}] 62 | }, 63 | { 64 | id: 12, 65 | children: [ 66 | {id: 121}, 67 | {id: 122}, 68 | {id: 123}, 69 | {id: 124}, 70 | {id: 125}, 71 | {id: 126}, 72 | {id: 127}, 73 | {id: 128}, 74 | {id: 129}, 75 | {id: 1210}, 76 | {id: 1211} 77 | ] 78 | } 79 | ] 80 | }); 81 | 82 | assert.isUndefined(root.parent); 83 | assert.isArray(root.children); 84 | assert.lengthOf(root.children, 2); 85 | assert.deepEqual(root.model, { 86 | id: 1, 87 | children: [ 88 | { 89 | id: 11, 90 | children: [{id: 111}] 91 | }, 92 | { 93 | id: 12, 94 | children: [ 95 | {id: 121}, 96 | {id: 122}, 97 | {id: 123}, 98 | {id: 124}, 99 | {id: 125}, 100 | {id: 126}, 101 | {id: 127}, 102 | {id: 128}, 103 | {id: 129}, 104 | {id: 1210}, 105 | {id: 1211} 106 | ] 107 | } 108 | ] 109 | }); 110 | 111 | assert.deepEqual(root, root.children[0].parent); 112 | assert.deepEqual(root, root.children[1].parent); 113 | 114 | node12 = root.children[1]; 115 | assert.isArray(node12.children); 116 | assert.lengthOf(node12.children, 11); 117 | assert.deepEqual(node12.model, { 118 | id: 12, 119 | children: [ 120 | {id: 121}, 121 | {id: 122}, 122 | {id: 123}, 123 | {id: 124}, 124 | {id: 125}, 125 | {id: 126}, 126 | {id: 127}, 127 | {id: 128}, 128 | {id: 129}, 129 | {id: 1210}, 130 | {id: 1211} 131 | ] 132 | }); 133 | 134 | assert.deepEqual(node12, node12.children[0].parent); 135 | assert.deepEqual(node12, node12.children[1].parent); 136 | }); 137 | }); 138 | 139 | describe('addChild()', function () { 140 | var root; 141 | 142 | beforeEach(function () { 143 | root = treeModel.parse({id: 1, children: [{id: 11}, {id: 12}]}); 144 | }); 145 | 146 | it('should add child to the end', function () { 147 | root.addChild(treeModel.parse({id: 13})); 148 | root.addChild(treeModel.parse({id: 10})); 149 | assert.deepEqual(root.model.children, [{id: 11}, {id: 12}, {id: 13}, {id: 10}]); 150 | }); 151 | 152 | it('should throw an error when child is not a Node', function () { 153 | assert.throws(root.addChild.bind(root, {children: []}), TypeError, 'Child must be of type Node.'); 154 | }); 155 | 156 | it('should add child at index', function () { 157 | root.addChildAtIndex(treeModel.parse({ id: 13 }), 1); 158 | assert.deepEqual(root.model.children, [{ id: 11 }, { id: 13 }, { id: 12 }]); 159 | assert.equal(root.children[1].model.id, 13); 160 | }); 161 | 162 | it('should add child at the end when index matches the children number', function () { 163 | root.addChildAtIndex(treeModel.parse({ id: 13 }), 2); 164 | assert.deepEqual(root.model.children, [{ id: 11 }, { id: 12 }, { id: 13 }]); 165 | }); 166 | 167 | it('should add child at index 0 of a leaf', function () { 168 | var leaf = root.first(idEq(11)); 169 | leaf.addChildAtIndex(treeModel.parse({ id: 111 }), 0); 170 | assert.deepEqual(leaf.model.children, [{ id: 111 }]); 171 | }); 172 | 173 | it('should throw an error when adding child at negative index', function () { 174 | var child; 175 | 176 | child = treeModel.parse({ id: 13 }); 177 | assert.throws(root.addChildAtIndex.bind(root, child, -1), Error, 'Invalid index.'); 178 | }); 179 | 180 | it('should throw an error when adding child at a too high index', function () { 181 | var child; 182 | 183 | child = treeModel.parse({ id: 13 }); 184 | assert.throws(root.addChildAtIndex.bind(root, child, 3), Error, 'Invalid index.'); 185 | }); 186 | }); 187 | 188 | describe('setIndex()', function () { 189 | var root; 190 | 191 | beforeEach(function () { 192 | root = treeModel.parse({id: 1, children: [{id: 11}, {id: 12}, {id:13}]}); 193 | }); 194 | 195 | it('should set the index of the node among its siblings', function () { 196 | var child, i; 197 | child = root.children[0]; 198 | for (i = 0; i < root.children.length; i++) { 199 | child.setIndex(i); 200 | assert.equal(child.getIndex(), i); 201 | assert.equal(root.model[child.config.childrenPropertyName].indexOf(child.model), i); 202 | } 203 | }); 204 | 205 | it('keeps the order of all other nodes', function () { 206 | var child, oldOrder, i, j, k, l; 207 | child = root.children[0]; 208 | for (i = 0; i < root.children.length; i++) { 209 | oldOrder = []; 210 | for (j = 0; j < root.children.length; j++) { 211 | if (root.children[j] !== child) { 212 | oldOrder.push(root.children[j]); 213 | } 214 | } 215 | 216 | child.setIndex(i); 217 | for (k = 0; k < root.children.length; k++) { 218 | for (l = 0; l < root.children.length; l++) { 219 | if (root.children[k] !== child && root.children[l] !== child) { 220 | assert.equal(k < l, oldOrder.indexOf(root.children[k]) < oldOrder.indexOf(root.children[l])); 221 | } 222 | } 223 | } 224 | } 225 | }); 226 | 227 | it('should return itself', function () { 228 | var child = root.children[0]; 229 | assert.equal(child.setIndex(1), child); 230 | }); 231 | 232 | it('should throw an error when node is a root and the index is not zero', function () { 233 | assert.throws(function () {root.setIndex(1);}, Error, 'Invalid index.'); 234 | }); 235 | 236 | it('should allow to set the root node index to zero', function () { 237 | assert.strictEqual(root.setIndex(0), root); 238 | }); 239 | 240 | it('should throw an error when setting to a negative index', function () { 241 | assert.throws(function () {root.children[0].setIndex(-1);}, Error, 'Invalid index.'); 242 | }); 243 | 244 | it('should throw an error when setting to a too high index', function () { 245 | assert.throws(function () {root.children[0].setIndex(root.children.length);}, Error, 'Invalid index.'); 246 | }); 247 | }); 248 | 249 | describe('getPath()', function () { 250 | var root; 251 | 252 | beforeEach(function () { 253 | root = treeModel.parse({ 254 | id: 1, 255 | children: [ 256 | { 257 | id: 11, 258 | children: [{id: 111}] 259 | }, 260 | { 261 | id: 12, 262 | children: [{id: 121}, {id: 122}] 263 | } 264 | ] 265 | }); 266 | }); 267 | 268 | it('should get an array with the root node if called on the root node', function () { 269 | var pathToRoot; 270 | pathToRoot = root.getPath(); 271 | assert.lengthOf(pathToRoot, 1); 272 | assert.strictEqual(pathToRoot[0].model.id, 1); 273 | }); 274 | 275 | it('should get an array of nodes from the root to the node (included)', function () { 276 | var pathToNode121; 277 | pathToNode121 = root.first(idEq(121)).getPath(); 278 | assert.lengthOf(pathToNode121, 3); 279 | assert.strictEqual(pathToNode121[0].model.id, 1); 280 | assert.strictEqual(pathToNode121[1].model.id, 12); 281 | assert.strictEqual(pathToNode121[2].model.id, 121); 282 | }); 283 | }); 284 | 285 | describe('traversal', function () { 286 | var root, spy121, spy12; 287 | 288 | function callback121(node) { 289 | if (node.model.id === 121) { 290 | return false; 291 | } 292 | } 293 | 294 | function callback12(node) { 295 | if (node.model.id === 12) { 296 | return false; 297 | } 298 | } 299 | 300 | beforeEach(function () { 301 | root = treeModel.parse({ 302 | id: 1, 303 | children: [ 304 | { 305 | id: 11, 306 | children: [{id: 111}] 307 | }, 308 | { 309 | id: 12, 310 | children: [{id: 121}, {id: 122}] 311 | } 312 | ] 313 | }); 314 | 315 | spy121 = sinon.spy(callback121); 316 | spy12 = sinon.spy(callback12); 317 | }); 318 | 319 | describe('walk depthFirstPreOrder by default', function () { 320 | it('should traverse the nodes until the callback returns false', function () { 321 | root.walk(spy121, this); 322 | assert.strictEqual(spy121.callCount, 5); 323 | assert(spy121.alwaysCalledOn(this)); 324 | assert(spy121.getCall(0).calledWithExactly(root.first(idEq(1)))); 325 | assert(spy121.getCall(1).calledWithExactly(root.first(idEq(11)))); 326 | assert(spy121.getCall(2).calledWithExactly(root.first(idEq(111)))); 327 | assert(spy121.getCall(3).calledWithExactly(root.first(idEq(12)))); 328 | assert(spy121.getCall(4).calledWithExactly(root.first(idEq(121)))); 329 | }); 330 | }); 331 | 332 | describe('walk depthFirstPostOrder', function () { 333 | it('should traverse the nodes until the callback returns false', function () { 334 | root.walk({strategy: 'post'}, spy121, this); 335 | assert.strictEqual(spy121.callCount, 3); 336 | assert(spy121.alwaysCalledOn(this)); 337 | assert(spy121.getCall(0).calledWithExactly(root.first(idEq(111)))); 338 | assert(spy121.getCall(1).calledWithExactly(root.first(idEq(11)))); 339 | assert(spy121.getCall(2).calledWithExactly(root.first(idEq(121)))); 340 | }); 341 | }); 342 | 343 | describe('walk depthFirstPostOrder (2)', function () { 344 | it('should traverse the nodes until the callback returns false', function () { 345 | root.walk({strategy: 'post'}, spy12, this); 346 | assert.strictEqual(spy12.callCount, 5); 347 | assert(spy12.alwaysCalledOn(this)); 348 | assert(spy12.getCall(0).calledWithExactly(root.first(idEq(111)))); 349 | assert(spy12.getCall(1).calledWithExactly(root.first(idEq(11)))); 350 | assert(spy12.getCall(2).calledWithExactly(root.first(idEq(121)))); 351 | assert(spy12.getCall(3).calledWithExactly(root.first(idEq(122)))); 352 | assert(spy12.getCall(4).calledWithExactly(root.first(idEq(12)))); 353 | }); 354 | }); 355 | 356 | describe('walk breadthFirst', function () { 357 | it('should traverse the nodes until the callback returns false', function () { 358 | root.walk({strategy: 'breadth'}, spy121, this); 359 | assert.strictEqual(spy121.callCount, 5); 360 | assert(spy121.alwaysCalledOn(this)); 361 | assert(spy121.getCall(0).calledWithExactly(root.first(idEq(1)))); 362 | assert(spy121.getCall(1).calledWithExactly(root.first(idEq(11)))); 363 | assert(spy121.getCall(2).calledWithExactly(root.first(idEq(12)))); 364 | assert(spy121.getCall(3).calledWithExactly(root.first(idEq(111)))); 365 | assert(spy121.getCall(4).calledWithExactly(root.first(idEq(121)))); 366 | }); 367 | }); 368 | 369 | describe('walk using unknown strategy', function () { 370 | it('should throw an error warning about the strategy', function () { 371 | assert.throws( 372 | root.walk.bind(root, {strategy: 'unknownStrategy'}, callback121, this), 373 | Error, 374 | 'Unknown tree walk strategy. Valid strategies are \'pre\' [default], \'post\' and \'breadth\'.'); 375 | }); 376 | }); 377 | }); 378 | 379 | describe('all()', function () { 380 | var root; 381 | 382 | beforeEach(function () { 383 | root = treeModel.parse({ 384 | id: 1, 385 | children: [ 386 | { 387 | id: 11, 388 | children: [{id: 111}] 389 | }, 390 | { 391 | id: 12, 392 | children: [{id: 121}, {id: 122}] 393 | } 394 | ] 395 | }); 396 | }); 397 | 398 | it('should get an empty array if no nodes match the predicate', function () { 399 | var idLt0; 400 | idLt0 = root.all(function (node) { 401 | return node.model.id < 0; 402 | }); 403 | assert.lengthOf(idLt0, 0); 404 | }); 405 | 406 | it('should get all nodes if no predicate is given', function () { 407 | var allNodes; 408 | allNodes = root.all(); 409 | assert.lengthOf(allNodes, 6); 410 | }); 411 | 412 | it('should get an array with the node itself if only the node matches the predicate', function () { 413 | var idEq1; 414 | idEq1 = root.all(idEq(1)); 415 | assert.lengthOf(idEq1, 1); 416 | assert.deepEqual(idEq1[0], root); 417 | }); 418 | 419 | it('should get an array with all nodes that match a given predicate', function () { 420 | var idGt100; 421 | idGt100 = root.all(function (node) { 422 | return node.model.id > 100; 423 | }); 424 | assert.lengthOf(idGt100, 3); 425 | assert.strictEqual(idGt100[0].model.id, 111); 426 | assert.strictEqual(idGt100[1].model.id, 121); 427 | assert.strictEqual(idGt100[2].model.id, 122); 428 | }); 429 | 430 | it('should get an array with all nodes that match a given predicate (2)', function () { 431 | var idGt10AndChildOfRoot; 432 | idGt10AndChildOfRoot = root.all(function (node) { 433 | return node.model.id > 10 && node.parent === root; 434 | }); 435 | assert.lengthOf(idGt10AndChildOfRoot, 2); 436 | assert.strictEqual(idGt10AndChildOfRoot[0].model.id, 11); 437 | assert.strictEqual(idGt10AndChildOfRoot[1].model.id, 12); 438 | }); 439 | }); 440 | 441 | describe('first()', function () { 442 | var root; 443 | 444 | beforeEach(function () { 445 | root = treeModel.parse({ 446 | id: 1, 447 | children: [ 448 | { 449 | id: 11, 450 | children: [{id: 111}] 451 | }, 452 | { 453 | id: 12, 454 | children: [{id: 121}, {id: 122}] 455 | } 456 | ] 457 | }); 458 | }); 459 | 460 | it('should get the first node when the predicate returns true', function () { 461 | var first; 462 | first = root.first(function () { 463 | return true; 464 | }); 465 | assert.equal(first.model.id, 1); 466 | }); 467 | 468 | it('should get the first node when no predicate is given', function () { 469 | var first; 470 | first = root.first(); 471 | assert.equal(first.model.id, 1); 472 | }); 473 | 474 | it('should get the first node with a different strategy when the predicate returns true', function () { 475 | var first; 476 | first = root.first({strategy: 'post'}, function () { 477 | return true; 478 | }); 479 | assert.equal(first.model.id, 111); 480 | }); 481 | 482 | it('should get the first node with a different strategy when no predicate is given', function () { 483 | var first; 484 | first = root.first({strategy: 'post'}); 485 | assert.equal(first.model.id, 111); 486 | }); 487 | }); 488 | 489 | describe('drop()', function () { 490 | var root; 491 | 492 | beforeEach(function () { 493 | root = treeModel.parse({ 494 | id: 1, 495 | children: [ 496 | { 497 | id: 11, 498 | children: [{id: 111}] 499 | }, 500 | { 501 | id: 12, 502 | children: [{id: 121}, {id: 122}] 503 | } 504 | ] 505 | }); 506 | }); 507 | 508 | it('should give back the dropped node, even if it is the root', function () { 509 | assert.deepEqual(root.drop(), root); 510 | }); 511 | 512 | it('should give back the dropped node, which no longer be found in the original root', function () { 513 | assert.deepEqual(root.first(idEq(11)).drop().model, {id: 11, children: [{id: 111}]}); 514 | assert.isUndefined(root.first(idEq(11))); 515 | }); 516 | }); 517 | 518 | describe('hasChildren()', function () { 519 | var root; 520 | 521 | beforeEach(function () { 522 | root = treeModel.parse({ 523 | id: 1, 524 | children: [ 525 | { 526 | id: 11, 527 | children: [{id: 111}] 528 | }, 529 | { 530 | id: 12, 531 | children: [{id: 121}, {id: 122}] 532 | } 533 | ] 534 | }); 535 | }); 536 | 537 | it('should return true for node with children', function () { 538 | assert.equal(root.hasChildren(), true); 539 | }); 540 | 541 | it('should return false for node without children', function () { 542 | assert.equal(root.first(idEq(111)).hasChildren(), false); 543 | }); 544 | }); 545 | }); 546 | 547 | describe('with custom children and comparator', function () { 548 | var treeModel; 549 | 550 | beforeEach(function () { 551 | treeModel = new TreeModel({ 552 | childrenPropertyName: 'deps', 553 | modelComparatorFn: function (a, b) { 554 | return b.id - a.id; 555 | } 556 | }); 557 | }); 558 | 559 | describe('parse()', function () { 560 | it('should create a root and stable sort the respective children according to the comparator', function () { 561 | var root, node12, i; 562 | 563 | root = treeModel.parse({ 564 | id: 1, 565 | deps: [ 566 | { 567 | id: 11, 568 | deps: [{id: 111}] 569 | }, 570 | { 571 | id: 12, 572 | deps: [ 573 | {id: 122, stable: 1}, 574 | {id: 121, stable: 1}, 575 | {id: 121, stable: 2}, 576 | {id: 121, stable: 3}, 577 | {id: 121, stable: 4}, 578 | {id: 121, stable: 5}, 579 | {id: 121, stable: 6}, 580 | {id: 121, stable: 7}, 581 | {id: 121, stable: 8}, 582 | {id: 121, stable: 9}, 583 | {id: 121, stable: 10}, 584 | {id: 121, stable: 11}, 585 | {id: 121, stable: 12}, 586 | {id: 121, stable: 13}, 587 | {id: 121, stable: 14}, 588 | {id: 121, stable: 15}, 589 | {id: 122, stable: 2} 590 | ] 591 | } 592 | ] 593 | }); 594 | 595 | assert.isUndefined(root.parent); 596 | assert.isArray(root.children); 597 | assert.lengthOf(root.children, 2); 598 | assert.deepEqual(root.model, { 599 | id: 1, 600 | deps: [ 601 | { 602 | id: 12, 603 | deps: [ 604 | {id: 122, stable: 1}, 605 | {id: 122, stable: 2}, 606 | {id: 121, stable: 1}, 607 | {id: 121, stable: 2}, 608 | {id: 121, stable: 3}, 609 | {id: 121, stable: 4}, 610 | {id: 121, stable: 5}, 611 | {id: 121, stable: 6}, 612 | {id: 121, stable: 7}, 613 | {id: 121, stable: 8}, 614 | {id: 121, stable: 9}, 615 | {id: 121, stable: 10}, 616 | {id: 121, stable: 11}, 617 | {id: 121, stable: 12}, 618 | {id: 121, stable: 13}, 619 | {id: 121, stable: 14}, 620 | {id: 121, stable: 15} 621 | ] 622 | }, 623 | { 624 | id: 11, 625 | deps: [{id: 111}] 626 | } 627 | ] 628 | }); 629 | 630 | assert.deepEqual(root, root.children[0].parent); 631 | assert.deepEqual(root, root.children[1].parent); 632 | 633 | node12 = root.children[0]; 634 | assert.isArray(node12.children); 635 | assert.lengthOf(node12.children, 17); 636 | assert.deepEqual(node12.model, { 637 | id: 12, 638 | deps: [ 639 | {id: 122, stable: 1}, 640 | {id: 122, stable: 2}, 641 | {id: 121, stable: 1}, 642 | {id: 121, stable: 2}, 643 | {id: 121, stable: 3}, 644 | {id: 121, stable: 4}, 645 | {id: 121, stable: 5}, 646 | {id: 121, stable: 6}, 647 | {id: 121, stable: 7}, 648 | {id: 121, stable: 8}, 649 | {id: 121, stable: 9}, 650 | {id: 121, stable: 10}, 651 | {id: 121, stable: 11}, 652 | {id: 121, stable: 12}, 653 | {id: 121, stable: 13}, 654 | {id: 121, stable: 14}, 655 | {id: 121, stable: 15} 656 | ] 657 | }); 658 | 659 | for (i = 0; i < 17; i++) { 660 | assert.deepEqual(node12, node12.children[i].parent); 661 | } 662 | }); 663 | }); 664 | 665 | describe('addChild()', function () { 666 | it('should add child respecting the given comparator', function () { 667 | var root; 668 | root = treeModel.parse({id: 1, deps: [ 669 | {id: 12, stable: 1}, 670 | {id: 11, stable: 1}, 671 | {id: 11, stable: 2}, 672 | {id: 11, stable: 3}, 673 | {id: 11, stable: 4}, 674 | {id: 11, stable: 5}, 675 | {id: 11, stable: 6}, 676 | {id: 11, stable: 7}, 677 | {id: 12, stable: 2}, 678 | {id: 11, stable: 8}, 679 | {id: 11, stable: 9}, 680 | {id: 11, stable: 10}, 681 | {id: 11, stable: 11}, 682 | {id: 11, stable: 12}, 683 | {id: 11, stable: 13}, 684 | {id: 11, stable: 14}, 685 | {id: 11, stable: 15}, 686 | {id: 13, stable: 1}, 687 | {id: 12, stable: 3} 688 | ]}); 689 | root.addChild(treeModel.parse({id: 13, stable: 2})); 690 | root.addChild(treeModel.parse({id: 10, stable: 1})); 691 | root.addChild(treeModel.parse({id: 10, stable: 2})); 692 | root.addChild(treeModel.parse({id: 12, stable: 4})); 693 | assert.lengthOf(root.children, 23); 694 | assert.deepEqual(root.model.deps, [ 695 | {id: 13, stable: 1}, 696 | {id: 13, stable: 2}, 697 | {id: 12, stable: 1}, 698 | {id: 12, stable: 2}, 699 | {id: 12, stable: 3}, 700 | {id: 12, stable: 4}, 701 | {id: 11, stable: 1}, 702 | {id: 11, stable: 2}, 703 | {id: 11, stable: 3}, 704 | {id: 11, stable: 4}, 705 | {id: 11, stable: 5}, 706 | {id: 11, stable: 6}, 707 | {id: 11, stable: 7}, 708 | {id: 11, stable: 8}, 709 | {id: 11, stable: 9}, 710 | {id: 11, stable: 10}, 711 | {id: 11, stable: 11}, 712 | {id: 11, stable: 12}, 713 | {id: 11, stable: 13}, 714 | {id: 11, stable: 14}, 715 | {id: 11, stable: 15}, 716 | {id: 10, stable: 1}, 717 | {id: 10, stable: 2} 718 | ]); 719 | }); 720 | 721 | it('should keep child nodes and model child nodes positions in sync', function () { 722 | var root; 723 | root = treeModel.parse({id: 1, deps: [{id: 12}, {id: 11}]}); 724 | root.addChild(treeModel.parse({id: 13})); 725 | root.addChild(treeModel.parse({id: 10})); 726 | assert.lengthOf(root.children, 4); 727 | assert.deepEqual(root.model.deps, [{id: 13}, {id: 12}, {id: 11}, {id: 10}]); 728 | 729 | assert.equal(root.children[0].model.id, 13); 730 | assert.equal(root.children[1].model.id, 12); 731 | assert.equal(root.children[2].model.id, 11); 732 | assert.equal(root.children[3].model.id, 10); 733 | }); 734 | 735 | it('should throw an error when adding child at index but a comparator was provided', function () { 736 | var root, child; 737 | 738 | root = treeModel.parse({id: 1, deps: [{id: 12}, {id: 11}]}); 739 | child = treeModel.parse({ id: 13 }); 740 | assert.throws( 741 | root.addChildAtIndex.bind(root, child, 1), 742 | Error, 743 | 'Cannot add child at index when using a comparator function.'); 744 | }); 745 | }); 746 | 747 | describe('setIndex()', function () { 748 | it('should throw an error when setting a node index but a comparator was provided', function () { 749 | var root, child; 750 | 751 | root = treeModel.parse({id: 1, deps: [{id: 12}, {id: 11}]}); 752 | child = root.children[0]; 753 | 754 | assert.throws( 755 | function () {child.setIndex(0);}, 756 | Error, 757 | 'Cannot set node index when using a comparator function.'); 758 | }); 759 | }); 760 | 761 | describe('drop()', function () { 762 | var root; 763 | 764 | beforeEach(function () { 765 | root = treeModel.parse({ 766 | id: 1, 767 | deps: [ 768 | { 769 | id: 11, 770 | deps: [{id: 111}] 771 | }, 772 | { 773 | id: 12, 774 | deps: [{id: 121}, {id: 122}] 775 | } 776 | ] 777 | }); 778 | }); 779 | 780 | it('should give back the dropped node, even if it is the root', function () { 781 | assert.deepEqual(root.drop(), root); 782 | }); 783 | 784 | it('should give back the dropped node, which no longer be found in the original root', function () { 785 | assert.deepEqual(root.first(idEq(11)).drop().model, {id: 11, deps: [{id: 111}]}); 786 | assert.isUndefined(root.first(idEq(11))); 787 | }); 788 | }); 789 | 790 | describe('hasChildren()', function () { 791 | var root; 792 | 793 | beforeEach(function () { 794 | root = treeModel.parse({ 795 | id: 1, 796 | deps: [ 797 | { 798 | id: 11, 799 | deps: [{id: 111}] 800 | }, 801 | { 802 | id: 12, 803 | deps: [{id: 121}, {id: 122}] 804 | } 805 | ] 806 | }); 807 | }); 808 | 809 | it('should return true for node with children', function () { 810 | assert.equal(root.hasChildren(), true); 811 | }); 812 | 813 | it('should return false for node without children', function () { 814 | assert.equal(root.first(idEq(111)).hasChildren(), false); 815 | }); 816 | }); 817 | }); 818 | }); 819 | --------------------------------------------------------------------------------