diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 38b8a96cc..000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "public/bower_components" -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c7b9408e4..77d664054 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules/ -public/bower_components/ +public/libs diff --git a/.travis.yml b/.travis.yml index 367fa5cd8..447d8e029 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,15 @@ language: node_js node_js: - - "0.11" - "0.10" + - "0.11" + - "0.12" + - "4" + - "5" + - "6" before_script: - npm install -g grunt-cli - npm install - - bower install -script: grunt test --verbose --force \ No newline at end of file +script: grunt test --verbose --force diff --git a/Gruntfile.js b/Gruntfile.js index 80a001d5b..a495f9555 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,11 +1,11 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), // the package file to use - + uglify: { files: { - expand: true, - flatten: true, + expand: true, + flatten: true, src: 'src/js/*.js', dest: 'dist', ext: '.min.js' @@ -32,18 +32,17 @@ module.exports = function(grunt) { }, copy: { - main: { + main: { files: [ - // copy dist to tests - // { expand: true, cwd: 'dist', src: '*', dest: 'tests/lib/' }, + // setup tests { expand: true, cwd: 'src/css', src: '*', dest: 'tests/lib/' }, { expand: true, cwd: 'src/js', src: '*', dest: 'tests/lib/' }, - // copy latest libs to tests - { expand: true, cwd: 'public/bower_components/jquery', src: 'jquery.js', dest: 'tests/lib/' }, - { expand: true, cwd: 'public/bower_components/bootstrap-datepicker/js', src: 'bootstrap-datepicker.js', dest: 'tests/lib/' }, - // copy src to example + { expand: true, cwd: 'node_modules/jquery/dist', src: 'jquery.js', dest: 'tests/lib/' }, + // setup public { expand: true, cwd: 'src/css', src: '*', dest: 'public/css/' }, - { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' } + { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' }, + { expand: true, cwd: 'node_modules/bootstrap/dist/', src: '**/*', dest: 'public/libs/bootstrap/' }, + { expand: true, cwd: 'node_modules/jquery/dist/', src: '*', dest: 'public/libs/jquery' } ] } } @@ -57,6 +56,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-copy'); // register one or more task lists (you should ALWAYS have a "default" task list) - grunt.registerTask('default', ['uglify','cssmin', 'copy', 'qunit', 'watch']); + grunt.registerTask('default', ['uglify', 'cssmin', 'copy', 'qunit', 'watch']); grunt.registerTask('test', 'qunit'); }; diff --git a/README.md b/README.md index c0a724235..58c5f3ac1 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,6 @@ If you want to do more, here's the full node specification selectedIcon: "glyphicon glyphicon-stop", color: "#000000", backColor: "#FFFFFF", - href: "#node-1", selectable: true, state: { checked: true, @@ -177,11 +176,6 @@ The foreground color used on a given node, overrides global color option. The background color used on a given node, overrides global color option. -#### href -`String` `Optional` - -Used in conjunction with global enableLinks option to specify anchor tag URL on a given node. - #### selectable `Boolean` `Default: true` @@ -231,7 +225,7 @@ Options allow you to customise the treeview's default appearance and behaviour. // expanded to 5 levels // with a background color of green $('#tree').treeview({ - data: data, // data is not optional + data: data, levels: 5, backColor: 'green' }); @@ -243,9 +237,24 @@ You can pass a new options object to the treeview at any time but this will have The following is a list of all available options. #### data -Array of Objects. No default, expects data +Array of Objects. No default, expects either data or dataUrl. + +This is the core data to be displayed by the tree view. If data is provided, dataUrl will be ignored. + +#### dataUrl +jQuery Ajax settings object, [as documented here](http://api.jquery.com/jquery.ajax/#jQuery-ajax-settings). + +Accepts a set of key/value pairs that configure an Ajax request. All settings are optional, any provided will be merge with the following default configuration. + +```javascript +{ + method: 'GET', + dataType: 'json', + cache: false +} +``` -This is the core data to be displayed by the tree view. +> JSON is the only format accepted. #### backColor String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: inherits from Bootstrap.css. @@ -277,11 +286,6 @@ String, class name(s). Default: "glyphicon" as defined by [Bootstrap Glyphicons Sets the icon to be used on a tree node with no child nodes. -#### enableLinks -Boolean. Default: false - -Whether or not to present node text as a hyperlink. The href value of which must be provided in the data structure on a per node basis. - #### expandIcon String, class name(s). Default: "glyphicon glyphicon-plus" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) @@ -317,6 +321,11 @@ String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal Sets the default background color activated when the users cursor hovers over a node. +#### preventUnselect +Boolean. Default: false + +Whether or not a node can be unselected without another node first being selected. + #### selectedIcon String, class name(s). Default: "glyphicon glyphicon-stop" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) @@ -368,6 +377,7 @@ String, class names(s). Default: "glyphicon glyphicon-unchecked" as defined by Sets the icon to be as an unchecked checkbox, used in conjunction with showCheckbox. + ## Methods Methods provide a way of interacting with the plugin programmatically. For example, expanding a node is possible via the expandNode method. @@ -399,10 +409,59 @@ $('#tree').data('treeview') ``` > A better approach, if you plan a lot of interaction. +If you intend to make multiple API calls, store a reference to the treeview instance. + +```javascript +var tree = $('#tree').treeview(true); +tree.method1(args); +tree.method2(args); +``` + + ### List of Methods The following is a list of all available methods. +> All methods that all declare argument `nodes` will accept either a single node, or an Array of nodes. + +#### addNode(nodes, parentNode, index, options) + +Add nodes to the tree. + +```javascript +$('#tree').treeview('addNode', [ nodes, parentNode, index, { silent: true } ]); +``` + +> If parentNode evaluates to false, node will be added to root + +> If index evaluates to false, node will be appended to the nodes + +Triggers `nodeRendered` event; pass silent to suppress events. + +#### addNodeAfter(nodes, node, options) + +Add nodes to the tree after given node. + +```javascript +$('#tree').treeview('addNodeAfter', [ nodes, node, { silent: true } ]); +``` + +> If node evaluates to false, node will be prepended to the tree's root + +Triggers `nodeRendered` event; pass silent to suppress events. + +#### addNodeBefore(nodes, node, options) + +Add nodes to the tree before given node. + +```javascript +$('#tree').treeview('addNodeAfter', [ nodes, node, { silent: true } ]); +``` + +> If node evaluates to false, node will be appended to the tree's root + +Triggers `nodeRendered` event; pass silent to suppress events. + #### checkAll(options) Checks all tree nodes @@ -413,12 +472,12 @@ $('#tree').treeview('checkAll', { silent: true }); Triggers `nodeChecked` event; pass silent to suppress events. -#### checkNode(node | nodeId, options) +#### checkNode(nodes, options) -Checks a given tree node, accepts node or nodeId. +Checks given tree nodes. ```javascript -$('#tree').treeview('checkNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('checkNode', [ nodes, { silent: true } ]); ``` Triggers `nodeChecked` event; pass silent to suppress events. @@ -443,12 +502,12 @@ $('#tree').treeview('collapseAll', { silent: true }); Triggers `nodeCollapsed` event; pass silent to suppress events. -#### collapseNode(node | nodeId, options) +#### collapseNode(nodes, options) Collapse a given tree node and it's child nodes. If you don't want to collapse the child nodes, pass option `{ ignoreChildren: true }`. ```javascript -$('#tree').treeview('collapseNode', [ nodeId, { silent: true, ignoreChildren: false } ]); +$('#tree').treeview('collapseNode', [ nodes, { silent: true, ignoreChildren: false } ]); ``` Triggers `nodeCollapsed` event; pass silent to suppress events. @@ -463,12 +522,12 @@ $('#tree').treeview('disableAll', { silent: true }); Triggers `nodeDisabled` event; pass silent to suppress events. -#### disableNode(node | nodeId, options) +#### disableNode(nodes, options) -Disable a given tree node, accepts node or nodeId. +Disable given tree nodes. ```javascript -$('#tree').treeview('disableNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('disableNode', [ nodes, { silent: true } ]); ``` Triggers `nodeDisabled` event; pass silent to suppress events. @@ -483,12 +542,12 @@ $('#tree').treeview('enableAll', { silent: true }); Triggers `nodeEnabled` event; pass silent to suppress events. -#### enableNode(node | nodeId, options) +#### enableNode(nodes, options) -Enable a given tree node, accepts node or nodeId. +Enable given tree nodes. ```javascript -$('#tree').treeview('enableNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('enableNode', [ nodes, { silent: true } ]); ``` Triggers `nodeEnabled` event; pass silent to suppress events. @@ -503,22 +562,40 @@ $('#tree').treeview('expandAll', { levels: 2, silent: true }); Triggers `nodeExpanded` event; pass silent to suppress events. -#### expandNode(node | nodeId, options) +#### expandNode(nodes, options) -Expand a given tree node, accepts node or nodeId. Optionally can be expanded to any given number of levels. +Expand given tree nodes. Optionally can be expanded to any given number of levels. ```javascript -$('#tree').treeview('expandNode', [ nodeId, { levels: 2, silent: true } ]); +$('#tree').treeview('expandNode', [ nodes, { levels: 2, silent: true } ]); ``` Triggers `nodeExpanded` event; pass silent to suppress events. +#### findNodes(pattern, field) + +Returns an array of matching node objects. + +```javascript +$('#tree').treeview('findNode', ['Parent', 'text']); +``` + +> Use regular expressions for pattern matching NOT string equals, if you need to match an exact string use start and end string anchors e.g. ^pattern$. + +#### getChecked() + +Returns an array of checked nodes e.g. state.checked = true. + +```javascript +$('#tree').treeview('getChecked'); +``` + #### getCollapsed() Returns an array of collapsed nodes e.g. state.expanded = false. ```javascript -$('#tree').treeview('getCollapsed', nodeId); +$('#tree').treeview('getCollapsed'); ``` #### getDisabled() @@ -526,7 +603,7 @@ $('#tree').treeview('getCollapsed', nodeId); Returns an array of disabled nodes e.g. state.disabled = true. ```javascript -$('#tree').treeview('getDisabled', nodeId); +$('#tree').treeview('getDisabled'); ``` #### getEnabled() @@ -534,7 +611,7 @@ $('#tree').treeview('getDisabled', nodeId); Returns an array of enabled nodes e.g. state.disabled = false. ```javascript -$('#tree').treeview('getEnabled', nodeId); +$('#tree').treeview('getEnabled'); ``` #### getExpanded() @@ -542,23 +619,23 @@ $('#tree').treeview('getEnabled', nodeId); Returns an array of expanded nodes e.g. state.expanded = true. ```javascript -$('#tree').treeview('getExpanded', nodeId); +$('#tree').treeview('getExpanded'); ``` -#### getNode(nodeId) +#### getNodes() -Returns a single node object that matches the given node id. +Returns an array of all initialized nodes. ```javascript -$('#tree').treeview('getNode', nodeId); +$('#tree').treeview('getNodes', nodes); ``` -#### getParent(node | nodeId) +#### getParents(nodes) -Returns the parent node of a given node, if valid otherwise returns undefined. +Returns parent nodes for given nodes, if valid otherwise returns undefined. ```javascript -$('#tree').treeview('getParent', node); +$('#tree').treeview('getParent', nodes); ``` #### getSelected() @@ -566,15 +643,23 @@ $('#tree').treeview('getParent', node); Returns an array of selected nodes e.g. state.selected = true. ```javascript -$('#tree').treeview('getSelected', nodeId); +$('#tree').treeview('getSelected'); ``` -#### getSiblings(node | nodeId) +#### getSiblings(nodes) -Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. +Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. ```javascript -$('#tree').treeview('getSiblings', node); +$('#tree').treeview('getSiblings', nodes); +``` + +#### getUnchecked() + +Returns an array of unchecked nodes e.g. state.checked = false. + +```javascript +$('#tree').treeview('getUnchecked'); ``` #### getUnselected() @@ -582,7 +667,7 @@ $('#tree').treeview('getSiblings', node); Returns an array of unselected nodes e.g. state.selected = false. ```javascript -$('#tree').treeview('getUnselected', nodeId); +$('#tree').treeview('getUnselected'); ``` #### remove() @@ -593,12 +678,20 @@ Removes the tree view component. Removing attached events, internal attached obj $('#tree').treeview('remove'); ``` -#### revealNode(node | nodeId, options) +#### removeNode() -Reveals a given tree node, expanding the tree from node to root. +Removes given nodes from the tree. ```javascript -$('#tree').treeview('revealNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('removeNode', [ nodes, { silent: true } ]); +``` + +#### revealNode(nodes, options) + +Reveals given tree nodes, expanding the tree from node to root. + +```javascript +$('#tree').treeview('revealNode', [ nodes, { silent: true } ]); ``` Triggers `nodeExpanded` event; pass silent to suppress events. @@ -619,52 +712,52 @@ $('#tree').treeview('search', [ 'Parent', { Triggers `searchComplete` event -#### selectNode(node | nodeId, options) +#### selectNode(nodes, options) -Selects a given tree node, accepts node or nodeId. +Selects given tree nodes. ```javascript -$('#tree').treeview('selectNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('selectNode', [ nodes, { silent: true } ]); ``` Triggers `nodeSelected` event; pass silent to suppress events. -#### toggleNodeChecked(node | nodeId, options) +#### toggleNodeChecked(nodes, options) -Toggles a nodes checked state; checking if unchecked, unchecking if checked. +Toggles a node's checked state; checking if unchecked, unchecking if checked. ```javascript -$('#tree').treeview('toggleNodeChecked', [ nodeId, { silent: true } ]); +$('#tree').treeview('toggleNodeChecked', [ nodes, { silent: true } ]); ``` Triggers either `nodeChecked` or `nodeUnchecked` event; pass silent to suppress events. -#### toggleNodeDisabled(node | nodeId, options) +#### toggleNodeDisabled(nodes, options) -Toggles a nodes disabled state; disabling if enabled, enabling if disabled. +Toggles a node's disabled state; disabling if enabled, enabling if disabled. ```javascript -$('#tree').treeview('toggleNodeDisabled', [ nodeId, { silent: true } ]); +$('#tree').treeview('toggleNodeDisabled', [ nodes, { silent: true } ]); ``` Triggers either `nodeDisabled` or `nodeEnabled` event; pass silent to suppress events. -#### toggleNodeExpanded(node | nodeId, options) +#### toggleNodeExpanded(nodes, options) -Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. +Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. ```javascript -$('#tree').treeview('toggleNodeExpanded', [ nodeId, { silent: true } ]); +$('#tree').treeview('toggleNodeExpanded', [ nodes, { silent: true } ]); ``` Triggers either `nodeExpanded` or `nodeCollapsed` event; pass silent to suppress events. -#### toggleNodeSelected(node | nodeId, options) +#### toggleNodeSelected(nodes, options) Toggles a node selected state; selecting if unselected, unselecting if selected. ```javascript -$('#tree').treeview('toggleNodeSelected', [ nodeId, { silent: true } ]); +$('#tree').treeview('toggleNodeSelected', [ nodes, { silent: true } ]); ``` Triggers either `nodeSelected` or `nodeUnselected` event; pass silent to suppress events. @@ -679,26 +772,38 @@ $('#tree').treeview('uncheckAll', { silent: true }); Triggers `nodeUnchecked` event; pass silent to suppress events. -#### uncheckNode(node | nodeId, options) +#### uncheckNode(nodes, options) -Uncheck a given tree node, accepts node or nodeId. +Uncheck given tree nodes. ```javascript -$('#tree').treeview('uncheckNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('uncheckNode', [ nodes, { silent: true } ]); ``` Triggers `nodeUnchecked` event; pass silent to suppress events. -#### unselectNode(node | nodeId, options) +#### updateNode(node, newNode, option) -Unselects a given tree node, accepts node or nodeId. +Updates / replaces a given tree node. ```javascript -$('#tree').treeview('unselectNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('updateNode', [ node, newNode, { silent: true } ]); +``` + +Triggers `nodeRendered` event; pass silent to suppress events. + +#### unselectNode(nodes, options) + +Unselects given tree nodes. + +```javascript +$('#tree').treeview('unselectNode', [ nodes, { silent: true } ]); ``` Triggers `nodeUnselected` event; pass silent to suppress events. + + ## Events Events are provided so that your application can respond to changes in the treeview's state. For example, if you want to update a display when a node is selected use the `nodeSelected` event. @@ -725,8 +830,27 @@ $('#tree').on('nodeSelected', function(event, data) { }); ``` + ### List of Events +#### Lifecycle Events + +> Use callback handlers for lifecycle events otherwise you'll miss the events fired during creation. + +`loading (event)` - The tree has initiated data loading. + +`loadingFailed (event, error)` - The tree failed to load data (ajax error) + +`initialized (event, nodes)` - The tree has initialized itself and data ready for rendering. + +`nodeRendered (event, node)` - A new node is rendered; + +`rendered (event, nodes)` - The tree is rendered; + +`destroyed (event)` The tree is being destroyed; + +#### State Events + `nodeChecked (event, node)` - A node is checked. `nodeCollapsed (event, node)` - A node is collapsed. @@ -743,10 +867,13 @@ $('#tree').on('nodeSelected', function(event, data) { `nodeUnselected (event, node)` - A node is unselected. +#### Other Events + `searchComplete (event, results)` - After a search completes `searchCleared (event, results)` - After search results are cleared +> All events that emit multiple nodes, do so as an object collection not an array. This is due to limitations of jQuery in cloning plain JavaScript objects. If you need an Array of nodes you'll need to reduce the object's values back into an array. ## Copyright and Licensing diff --git a/bower.json b/bower.json index 1b009d7da..c20725b49 100644 --- a/bower.json +++ b/bower.json @@ -3,10 +3,7 @@ "description": "Tree View for Twitter Bootstrap", "version": "1.2.0", "homepage": "https://github.com/jonmiles/bootstrap-treeview", - "main": [ - "dist/bootstrap-treeview.min.js", - "dist/bootstrap-treeview.min.css" - ], + "main": "dist/bootstrap-treeview.min.js", "keywords": [ "twitter", "bootstrap", @@ -21,7 +18,6 @@ "ignore": [ "**/.*", "node_modules", - "bower_components", "test", "tests" ], diff --git a/dist/bootstrap-treeview.min.css b/dist/bootstrap-treeview.min.css index 57a348a87..b64a53bec 100644 --- a/dist/bootstrap-treeview.min.css +++ b/dist/bootstrap-treeview.min.css @@ -1 +1 @@ -.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed} \ No newline at end of file +.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}.treeview .node-hidden{display:none} \ No newline at end of file diff --git a/dist/bootstrap-treeview.min.js b/dist/bootstrap-treeview.min.js index 9e2803815..e86b764f4 100644 --- a/dist/bootstrap-treeview.min.js +++ b/dist/bootstrap-treeview.min.js @@ -1 +1 @@ -!function(a,b,c,d){"use strict";var e="treeview",f={};f.settings={injectStyle:!0,levels:2,expandIcon:"glyphicon glyphicon-plus",collapseIcon:"glyphicon glyphicon-minus",emptyIcon:"glyphicon",nodeIcon:"",selectedIcon:"",checkedIcon:"glyphicon glyphicon-check",uncheckedIcon:"glyphicon glyphicon-unchecked",color:d,backColor:d,borderColor:d,onhoverColor:"#F5F5F5",selectedColor:"#FFFFFF",selectedBackColor:"#428bca",searchResultColor:"#D9534F",searchResultBackColor:d,enableLinks:!1,highlightSelected:!0,highlightSearchResults:!0,showBorder:!0,showIcon:!0,showCheckbox:!1,showTags:!1,multiSelect:!1,onNodeChecked:d,onNodeCollapsed:d,onNodeDisabled:d,onNodeEnabled:d,onNodeExpanded:d,onNodeSelected:d,onNodeUnchecked:d,onNodeUnselected:d,onSearchComplete:d,onSearchCleared:d},f.options={silent:!1,ignoreChildren:!1},f.searchOptions={ignoreCase:!0,exactMatch:!1,revealResults:!0};var g=function(b,c){return this.$element=a(b),this.elementId=b.id,this.styleId=this.elementId+"-style",this.init(c),{options:this.options,init:a.proxy(this.init,this),remove:a.proxy(this.remove,this),getNode:a.proxy(this.getNode,this),getParent:a.proxy(this.getParent,this),getSiblings:a.proxy(this.getSiblings,this),getSelected:a.proxy(this.getSelected,this),getUnselected:a.proxy(this.getUnselected,this),getExpanded:a.proxy(this.getExpanded,this),getCollapsed:a.proxy(this.getCollapsed,this),getChecked:a.proxy(this.getChecked,this),getUnchecked:a.proxy(this.getUnchecked,this),getDisabled:a.proxy(this.getDisabled,this),getEnabled:a.proxy(this.getEnabled,this),selectNode:a.proxy(this.selectNode,this),unselectNode:a.proxy(this.unselectNode,this),toggleNodeSelected:a.proxy(this.toggleNodeSelected,this),collapseAll:a.proxy(this.collapseAll,this),collapseNode:a.proxy(this.collapseNode,this),expandAll:a.proxy(this.expandAll,this),expandNode:a.proxy(this.expandNode,this),toggleNodeExpanded:a.proxy(this.toggleNodeExpanded,this),revealNode:a.proxy(this.revealNode,this),checkAll:a.proxy(this.checkAll,this),checkNode:a.proxy(this.checkNode,this),uncheckAll:a.proxy(this.uncheckAll,this),uncheckNode:a.proxy(this.uncheckNode,this),toggleNodeChecked:a.proxy(this.toggleNodeChecked,this),disableAll:a.proxy(this.disableAll,this),disableNode:a.proxy(this.disableNode,this),enableAll:a.proxy(this.enableAll,this),enableNode:a.proxy(this.enableNode,this),toggleNodeDisabled:a.proxy(this.toggleNodeDisabled,this),search:a.proxy(this.search,this),clearSearch:a.proxy(this.clearSearch,this)}};g.prototype.init=function(b){this.tree=[],this.nodes=[],b.data&&("string"==typeof b.data&&(b.data=a.parseJSON(b.data)),this.tree=a.extend(!0,[],b.data),delete b.data),this.options=a.extend({},f.settings,b),this.destroy(),this.subscribeEvents(),this.setInitialStates({nodes:this.tree},0),this.render()},g.prototype.remove=function(){this.destroy(),a.removeData(this,e),a("#"+this.styleId).remove()},g.prototype.destroy=function(){this.initialized&&(this.$wrapper.remove(),this.$wrapper=null,this.unsubscribeEvents(),this.initialized=!1)},g.prototype.unsubscribeEvents=function(){this.$element.off("click"),this.$element.off("nodeChecked"),this.$element.off("nodeCollapsed"),this.$element.off("nodeDisabled"),this.$element.off("nodeEnabled"),this.$element.off("nodeExpanded"),this.$element.off("nodeSelected"),this.$element.off("nodeUnchecked"),this.$element.off("nodeUnselected"),this.$element.off("searchComplete"),this.$element.off("searchCleared")},g.prototype.subscribeEvents=function(){this.unsubscribeEvents(),this.$element.on("click",a.proxy(this.clickHandler,this)),"function"==typeof this.options.onNodeChecked&&this.$element.on("nodeChecked",this.options.onNodeChecked),"function"==typeof this.options.onNodeCollapsed&&this.$element.on("nodeCollapsed",this.options.onNodeCollapsed),"function"==typeof this.options.onNodeDisabled&&this.$element.on("nodeDisabled",this.options.onNodeDisabled),"function"==typeof this.options.onNodeEnabled&&this.$element.on("nodeEnabled",this.options.onNodeEnabled),"function"==typeof this.options.onNodeExpanded&&this.$element.on("nodeExpanded",this.options.onNodeExpanded),"function"==typeof this.options.onNodeSelected&&this.$element.on("nodeSelected",this.options.onNodeSelected),"function"==typeof this.options.onNodeUnchecked&&this.$element.on("nodeUnchecked",this.options.onNodeUnchecked),"function"==typeof this.options.onNodeUnselected&&this.$element.on("nodeUnselected",this.options.onNodeUnselected),"function"==typeof this.options.onSearchComplete&&this.$element.on("searchComplete",this.options.onSearchComplete),"function"==typeof this.options.onSearchCleared&&this.$element.on("searchCleared",this.options.onSearchCleared)},g.prototype.setInitialStates=function(b,c){if(b.nodes){c+=1;var d=b,e=this;a.each(b.nodes,function(a,b){b.nodeId=e.nodes.length,b.parentId=d.nodeId,b.hasOwnProperty("selectable")||(b.selectable=!0),b.state=b.state||{},b.state.hasOwnProperty("checked")||(b.state.checked=!1),b.state.hasOwnProperty("disabled")||(b.state.disabled=!1),b.state.hasOwnProperty("expanded")||(!b.state.disabled&&c0?b.state.expanded=!0:b.state.expanded=!1),b.state.hasOwnProperty("selected")||(b.state.selected=!1),e.nodes.push(b),b.nodes&&e.setInitialStates(b,c)})}},g.prototype.clickHandler=function(b){this.options.enableLinks||b.preventDefault();var c=a(b.target),d=this.findNode(c);if(d&&!d.state.disabled){var e=c.attr("class")?c.attr("class").split(" "):[];-1!==e.indexOf("expand-icon")?(this.toggleExpandedState(d,f.options),this.render()):-1!==e.indexOf("check-icon")?(this.toggleCheckedState(d,f.options),this.render()):(d.selectable?this.toggleSelectedState(d,f.options):this.toggleExpandedState(d,f.options),this.render())}},g.prototype.findNode=function(a){var b=a.closest("li.list-group-item").attr("data-nodeid"),c=this.nodes[b];return c||console.log("Error: node does not exist"),c},g.prototype.toggleExpandedState=function(a,b){a&&this.setExpandedState(a,!a.state.expanded,b)},g.prototype.setExpandedState=function(b,c,d){c!==b.state.expanded&&(c&&b.nodes?(b.state.expanded=!0,d.silent||this.$element.trigger("nodeExpanded",a.extend(!0,{},b))):c||(b.state.expanded=!1,d.silent||this.$element.trigger("nodeCollapsed",a.extend(!0,{},b)),b.nodes&&!d.ignoreChildren&&a.each(b.nodes,a.proxy(function(a,b){this.setExpandedState(b,!1,d)},this))))},g.prototype.toggleSelectedState=function(a,b){a&&this.setSelectedState(a,!a.state.selected,b)},g.prototype.setSelectedState=function(b,c,d){c!==b.state.selected&&(c?(this.options.multiSelect||a.each(this.findNodes("true","g","state.selected"),a.proxy(function(a,b){this.setSelectedState(b,!1,d)},this)),b.state.selected=!0,d.silent||this.$element.trigger("nodeSelected",a.extend(!0,{},b))):(b.state.selected=!1,d.silent||this.$element.trigger("nodeUnselected",a.extend(!0,{},b))))},g.prototype.toggleCheckedState=function(a,b){a&&this.setCheckedState(a,!a.state.checked,b)},g.prototype.setCheckedState=function(b,c,d){c!==b.state.checked&&(c?(b.state.checked=!0,d.silent||this.$element.trigger("nodeChecked",a.extend(!0,{},b))):(b.state.checked=!1,d.silent||this.$element.trigger("nodeUnchecked",a.extend(!0,{},b))))},g.prototype.setDisabledState=function(b,c,d){c!==b.state.disabled&&(c?(b.state.disabled=!0,this.setExpandedState(b,!1,d),this.setSelectedState(b,!1,d),this.setCheckedState(b,!1,d),d.silent||this.$element.trigger("nodeDisabled",a.extend(!0,{},b))):(b.state.disabled=!1,d.silent||this.$element.trigger("nodeEnabled",a.extend(!0,{},b))))},g.prototype.render=function(){this.initialized||(this.$element.addClass(e),this.$wrapper=a(this.template.list),this.injectStyle(),this.initialized=!0),this.$element.empty().append(this.$wrapper.empty()),this.buildTree(this.tree,0)},g.prototype.buildTree=function(b,c){if(b){c+=1;var d=this;a.each(b,function(b,e){for(var f=a(d.template.item).addClass("node-"+d.elementId).addClass(e.state.checked?"node-checked":"").addClass(e.state.disabled?"node-disabled":"").addClass(e.state.selected?"node-selected":"").addClass(e.searchResult?"search-result":"").attr("data-nodeid",e.nodeId).attr("style",d.buildStyleOverride(e)),g=0;c-1>g;g++)f.append(d.template.indent);var h=[];if(e.nodes?(h.push("expand-icon"),h.push(e.state.expanded?d.options.collapseIcon:d.options.expandIcon)):h.push(d.options.emptyIcon),f.append(a(d.template.icon).addClass(h.join(" "))),d.options.showIcon){var h=["node-icon"];h.push(e.icon||d.options.nodeIcon),e.state.selected&&(h.pop(),h.push(e.selectedIcon||d.options.selectedIcon||e.icon||d.options.nodeIcon)),f.append(a(d.template.icon).addClass(h.join(" ")))}if(d.options.showCheckbox){var h=["check-icon"];h.push(e.state.checked?d.options.checkedIcon:d.options.uncheckedIcon),f.append(a(d.template.icon).addClass(h.join(" ")))}return f.append(d.options.enableLinks?a(d.template.link).attr("href",e.href).append(e.text):e.text),d.options.showTags&&e.tags&&a.each(e.tags,function(b,c){f.append(a(d.template.badge).append(c))}),d.$wrapper.append(f),e.nodes&&e.state.expanded&&!e.state.disabled?d.buildTree(e.nodes,c):void 0})}},g.prototype.buildStyleOverride=function(a){if(a.state.disabled)return"";var b=a.color,c=a.backColor;return this.options.highlightSelected&&a.state.selected&&(this.options.selectedColor&&(b=this.options.selectedColor),this.options.selectedBackColor&&(c=this.options.selectedBackColor)),this.options.highlightSearchResults&&a.searchResult&&!a.state.disabled&&(this.options.searchResultColor&&(b=this.options.searchResultColor),this.options.searchResultBackColor&&(c=this.options.searchResultBackColor)),"color:"+b+";background-color:"+c+";"},g.prototype.injectStyle=function(){this.options.injectStyle&&!c.getElementById(this.styleId)&&a('").appendTo("head")},g.prototype.buildStyle=function(){var a=".node-"+this.elementId+"{";return this.options.color&&(a+="color:"+this.options.color+";"),this.options.backColor&&(a+="background-color:"+this.options.backColor+";"),this.options.showBorder?this.options.borderColor&&(a+="border:1px solid "+this.options.borderColor+";"):a+="border:none;",a+="}",this.options.onhoverColor&&(a+=".node-"+this.elementId+":not(.node-disabled):hover{background-color:"+this.options.onhoverColor+";}"),this.css+a},g.prototype.template={list:'',item:'
  • ',indent:'',icon:'',link:'',badge:''},g.prototype.css=".treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}",g.prototype.getNode=function(a){return this.nodes[a]},g.prototype.getParent=function(a){var b=this.identifyNode(a);return this.nodes[b.parentId]},g.prototype.getSiblings=function(a){var b=this.identifyNode(a),c=this.getParent(b),d=c?c.nodes:this.tree;return d.filter(function(a){return a.nodeId!==b.nodeId})},g.prototype.getSelected=function(){return this.findNodes("true","g","state.selected")},g.prototype.getUnselected=function(){return this.findNodes("false","g","state.selected")},g.prototype.getExpanded=function(){return this.findNodes("true","g","state.expanded")},g.prototype.getCollapsed=function(){return this.findNodes("false","g","state.expanded")},g.prototype.getChecked=function(){return this.findNodes("true","g","state.checked")},g.prototype.getUnchecked=function(){return this.findNodes("false","g","state.checked")},g.prototype.getDisabled=function(){return this.findNodes("true","g","state.disabled")},g.prototype.getEnabled=function(){return this.findNodes("false","g","state.disabled")},g.prototype.selectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!0,b)},this)),this.render()},g.prototype.unselectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeSelected=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleSelectedState(a,b)},this)),this.render()},g.prototype.collapseAll=function(b){var c=this.findNodes("true","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.collapseNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.expandAll=function(b){if(b=a.extend({},f.options,b),b&&b.levels)this.expandLevels(this.tree,b.levels,b);else{var c=this.findNodes("false","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!0,b)},this))}this.render()},g.prototype.expandNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!0,b),a.nodes&&b&&b.levels&&this.expandLevels(a.nodes,b.levels-1,b)},this)),this.render()},g.prototype.expandLevels=function(b,c,d){d=a.extend({},f.options,d),a.each(b,a.proxy(function(a,b){this.setExpandedState(b,c>0?!0:!1,d),b.nodes&&this.expandLevels(b.nodes,c-1,d)},this))},g.prototype.revealNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){for(var c=this.getParent(a);c;)this.setExpandedState(c,!0,b),c=this.getParent(c)},this)),this.render()},g.prototype.toggleNodeExpanded=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleExpandedState(a,b)},this)),this.render()},g.prototype.checkAll=function(b){var c=this.findNodes("false","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.checkNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.uncheckAll=function(b){var c=this.findNodes("true","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.uncheckNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeChecked=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleCheckedState(a,b)},this)),this.render()},g.prototype.disableAll=function(b){var c=this.findNodes("false","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.disableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.enableAll=function(b){var c=this.findNodes("true","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.enableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeDisabled=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!a.state.disabled,b)},this)),this.render()},g.prototype.forEachIdentifier=function(b,c,d){c=a.extend({},f.options,c),b instanceof Array||(b=[b]),a.each(b,a.proxy(function(a,b){d(this.identifyNode(b),c)},this))},g.prototype.identifyNode=function(a){return"number"==typeof a?this.nodes[a]:a},g.prototype.search=function(b,c){c=a.extend({},f.searchOptions,c),this.clearSearch({render:!1});var d=[];if(b&&b.length>0){c.exactMatch&&(b="^"+b+"$");var e="g";c.ignoreCase&&(e+="i"),d=this.findNodes(b,e),a.each(d,function(a,b){b.searchResult=!0})}return c.revealResults?this.revealNode(d):this.render(),this.$element.trigger("searchComplete",a.extend(!0,{},d)),d},g.prototype.clearSearch=function(b){b=a.extend({},{render:!0},b);var c=a.each(this.findNodes("true","g","searchResult"),function(a,b){b.searchResult=!1});b.render&&this.render(),this.$element.trigger("searchCleared",a.extend(!0,{},c))},g.prototype.findNodes=function(b,c,d){c=c||"g",d=d||"text";var e=this;return a.grep(this.nodes,function(a){var f=e.getNodeValue(a,d);return"string"==typeof f?f.match(new RegExp(b,c)):void 0})},g.prototype.getNodeValue=function(a,b){var c=b.indexOf(".");if(c>0){var e=a[b.substring(0,c)],f=b.substring(c+1,b.length);return this.getNodeValue(e,f)}return a.hasOwnProperty(b)?a[b].toString():d};var h=function(a){b.console&&b.console.error(a)};a.fn[e]=function(b,c){var d;return this.each(function(){var f=a.data(this,e);"string"==typeof b?f?a.isFunction(f[b])&&"_"!==b.charAt(0)?(c instanceof Array||(c=[c]),d=f[b].apply(f,c)):h("No such method : "+b):h("Not initialized, can not call method : "+b):"boolean"==typeof b?d=f:a.data(this,e,new g(this,a.extend(!0,{},b)))}),d||this}}(jQuery,window,document); \ No newline at end of file +!function(a,b,c,d){"use strict";var e="treeview",f={};f.settings={injectStyle:!0,levels:2,expandIcon:"glyphicon glyphicon-plus",collapseIcon:"glyphicon glyphicon-minus",emptyIcon:"glyphicon",nodeIcon:"",selectedIcon:"",checkedIcon:"glyphicon glyphicon-check",uncheckedIcon:"glyphicon glyphicon-unchecked",color:d,backColor:d,borderColor:d,onhoverColor:"#F5F5F5",selectedColor:"#FFFFFF",selectedBackColor:"#428bca",searchResultColor:"#D9534F",searchResultBackColor:d,highlightSelected:!0,highlightSearchResults:!0,showBorder:!0,showIcon:!0,showCheckbox:!1,showTags:!1,multiSelect:!1,preventUnselect:!1,onLoading:d,onLoadingFailed:d,onInitialized:d,onNodeRendered:d,onRendered:d,onDestroyed:d,onNodeChecked:d,onNodeCollapsed:d,onNodeDisabled:d,onNodeEnabled:d,onNodeExpanded:d,onNodeSelected:d,onNodeUnchecked:d,onNodeUnselected:d,onSearchComplete:d,onSearchCleared:d},f.options={silent:!1,ignoreChildren:!1},f.searchOptions={ignoreCase:!0,exactMatch:!1,revealResults:!0},f.dataUrl={method:"GET",dataType:"json",cache:!1};var g=function(b,c){return this.$element=a(b),this._elementId=b.id,this._styleId=this._elementId+"-style",this._init(c),{options:this._options,init:a.proxy(this._init,this),remove:a.proxy(this._remove,this),findNodes:a.proxy(this.findNodes,this),getNodes:a.proxy(this.getNodes,this),getParents:a.proxy(this.getParents,this),getSiblings:a.proxy(this.getSiblings,this),getSelected:a.proxy(this.getSelected,this),getUnselected:a.proxy(this.getUnselected,this),getExpanded:a.proxy(this.getExpanded,this),getCollapsed:a.proxy(this.getCollapsed,this),getChecked:a.proxy(this.getChecked,this),getUnchecked:a.proxy(this.getUnchecked,this),getDisabled:a.proxy(this.getDisabled,this),getEnabled:a.proxy(this.getEnabled,this),addNode:a.proxy(this.addNode,this),addNodeAfter:a.proxy(this.addNodeAfter,this),addNodeBefore:a.proxy(this.addNodeBefore,this),removeNode:a.proxy(this.removeNode,this),updateNode:a.proxy(this.updateNode,this),selectNode:a.proxy(this.selectNode,this),unselectNode:a.proxy(this.unselectNode,this),toggleNodeSelected:a.proxy(this.toggleNodeSelected,this),collapseAll:a.proxy(this.collapseAll,this),collapseNode:a.proxy(this.collapseNode,this),expandAll:a.proxy(this.expandAll,this),expandNode:a.proxy(this.expandNode,this),toggleNodeExpanded:a.proxy(this.toggleNodeExpanded,this),revealNode:a.proxy(this.revealNode,this),checkAll:a.proxy(this.checkAll,this),checkNode:a.proxy(this.checkNode,this),uncheckAll:a.proxy(this.uncheckAll,this),uncheckNode:a.proxy(this.uncheckNode,this),toggleNodeChecked:a.proxy(this.toggleNodeChecked,this),disableAll:a.proxy(this.disableAll,this),disableNode:a.proxy(this.disableNode,this),enableAll:a.proxy(this.enableAll,this),enableNode:a.proxy(this.enableNode,this),toggleNodeDisabled:a.proxy(this.toggleNodeDisabled,this),search:a.proxy(this.search,this),clearSearch:a.proxy(this.clearSearch,this)}};g.prototype._init=function(b){this._tree=[],this._initialized=!1,this._options=a.extend({},f.settings,b),this._destroy(),this._subscribeEvents(),this._triggerEvent("loading",null,f.options),this._load(b).then(a.proxy(function(b){return this._tree=a.extend(!0,[],b)},this),a.proxy(function(a){this._triggerEvent("loadingFailed",a,f.options)},this)).then(a.proxy(function(a){return this._setInitialStates({nodes:a},0)},this)).then(a.proxy(function(){this._render()},this))},g.prototype._load=function(b){var c=new a.Deferred;return b.data?this._loadLocalData(b,c):b.dataUrl&&this._loadRemoteData(b,c),c.promise()},g.prototype._loadRemoteData=function(b,c){a.ajax(a.extend(!0,{},f.dataUrl,b.dataUrl)).done(function(a){c.resolve(a)}).fail(function(a,b,d){c.reject(d)})},g.prototype._loadLocalData=function(b,c){c.resolve("string"==typeof b.data?a.parseJSON(b.data):a.extend(!0,[],b.data))},g.prototype._remove=function(){this._destroy(),a.removeData(this,e),a("#"+this._styleId).remove()},g.prototype._destroy=function(){this._initialized&&(this._initialized=!1,this._triggerEvent("destroyed",null,f.options),this._unsubscribeEvents(),this.$wrapper.remove(),this.$wrapper=null)},g.prototype._unsubscribeEvents=function(){this.$element.off("loading"),this.$element.off("loadingFailed"),this.$element.off("initialized"),this.$element.off("nodeRendered"),this.$element.off("rendered"),this.$element.off("destroyed"),this.$element.off("click"),this.$element.off("nodeChecked"),this.$element.off("nodeCollapsed"),this.$element.off("nodeDisabled"),this.$element.off("nodeEnabled"),this.$element.off("nodeExpanded"),this.$element.off("nodeSelected"),this.$element.off("nodeUnchecked"),this.$element.off("nodeUnselected"),this.$element.off("searchComplete"),this.$element.off("searchCleared")},g.prototype._subscribeEvents=function(){this._unsubscribeEvents(),"function"==typeof this._options.onLoading&&this.$element.on("loading",this._options.onLoading),"function"==typeof this._options.onLoadingFailed&&this.$element.on("loadingFailed",this._options.onLoadingFailed),"function"==typeof this._options.onInitialized&&this.$element.on("initialized",this._options.onInitialized),"function"==typeof this._options.onNodeRendered&&this.$element.on("nodeRendered",this._options.onNodeRendered),"function"==typeof this._options.onRendered&&this.$element.on("rendered",this._options.onRendered),"function"==typeof this._options.onDestroyed&&this.$element.on("destroyed",this._options.onDestroyed),this.$element.on("click",a.proxy(this._clickHandler,this)),"function"==typeof this._options.onNodeChecked&&this.$element.on("nodeChecked",this._options.onNodeChecked),"function"==typeof this._options.onNodeCollapsed&&this.$element.on("nodeCollapsed",this._options.onNodeCollapsed),"function"==typeof this._options.onNodeDisabled&&this.$element.on("nodeDisabled",this._options.onNodeDisabled),"function"==typeof this._options.onNodeEnabled&&this.$element.on("nodeEnabled",this._options.onNodeEnabled),"function"==typeof this._options.onNodeExpanded&&this.$element.on("nodeExpanded",this._options.onNodeExpanded),"function"==typeof this._options.onNodeSelected&&this.$element.on("nodeSelected",this._options.onNodeSelected),"function"==typeof this._options.onNodeUnchecked&&this.$element.on("nodeUnchecked",this._options.onNodeUnchecked),"function"==typeof this._options.onNodeUnselected&&this.$element.on("nodeUnselected",this._options.onNodeUnselected),"function"==typeof this._options.onSearchComplete&&this.$element.on("searchComplete",this._options.onSearchComplete),"function"==typeof this._options.onSearchCleared&&this.$element.on("searchCleared",this._options.onSearchCleared)},g.prototype._triggerEvent=function(b,c,d){d&&!d.silent&&this.$element.trigger(b,a.extend(!0,{},c))},g.prototype._setInitialStates=function(b,c){return this._nodes={},a.when.apply(this,this._setInitialState(b,c)).done(a.proxy(function(){this._orderedNodes=this._sortNodes(),this._triggerEvent("initialized",this._orderedNodes,f.options)},this))},g.prototype._setInitialState=function(b,c,d){if(b.nodes){c+=1,d=d||[];var e=b;return a.each(b.nodes,a.proxy(function(b,f){var g=new a.Deferred;d.push(g.promise()),f.level=c,f.index=b,f.nodeId=e&&e.nodeId?e.nodeId+"."+f.index:c-1+"."+f.index,f.parentId=e.nodeId,f.hasOwnProperty("selectable")||(f.selectable=!0),f.state=f.state||{},f.state.hasOwnProperty("checked")||(f.state.checked=!1),f.state.hasOwnProperty("disabled")||(f.state.disabled=!1),f.state.hasOwnProperty("expanded")||(!f.state.disabled&&c0?f.state.expanded=!0:f.state.expanded=!1),f.state.hasOwnProperty("selected")||(f.state.selected=!1),e&&e.state&&e.state.expanded||c<=this._options.levels?f.state.visible=!0:f.state.visible=!1,f.nodes&&(f.nodes.length>0?this._setInitialState(f,c,d):delete f.nodes),this._nodes[f.nodeId]=f,g.resolve()},this)),d}},g.prototype._sortNodes=function(){return a.map(Object.keys(this._nodes).sort(),a.proxy(function(a,b){return this._nodes[a]},this))},g.prototype._clickHandler=function(b){var c=a(b.target),d=this.targetNode(c);if(d&&!d.state.disabled){var e=c.attr("class")?c.attr("class").split(" "):[];-1!==e.indexOf("expand-icon")?this._toggleExpanded(d,a.extend({},f.options)):-1!==e.indexOf("check-icon")?this._toggleChecked(d,a.extend({},f.options)):d.selectable?this._toggleSelected(d,a.extend({},f.options)):this._toggleExpanded(d,a.extend({},f.options))}},g.prototype.targetNode=function(a){var b=a.closest("li.list-group-item").attr("data-nodeId"),c=this._nodes[b];return c||console.log("Error: node does not exist"),c},g.prototype._toggleExpanded=function(a,b){a&&this._setExpanded(a,!a.state.expanded,b)},g.prototype._setExpanded=function(b,c,d){d&&c===b.state.expanded||(c&&b.nodes?(b.state.expanded=!0,b.$el&&b.$el.children("span.expand-icon").removeClass(this._options.expandIcon).addClass(this._options.collapseIcon),b.nodes&&d&&a.each(b.nodes,a.proxy(function(a,b){this._setVisible(b,!0,d)},this)),this._triggerEvent("nodeExpanded",b,d)):c||(b.state.expanded=!1,b.$el&&b.$el.children("span.expand-icon").removeClass(this._options.collapseIcon).addClass(this._options.expandIcon),b.nodes&&d&&a.each(b.nodes,a.proxy(function(a,b){this._setVisible(b,!1,d),this._setExpanded(b,!1,d)},this)),this._triggerEvent("nodeCollapsed",b,d)))},g.prototype._setVisible=function(a,b,c){c&&b===a.state.visible||(b?(a.state.visible=!0,a.$el&&a.$el.removeClass("node-hidden")):(a.state.visible=!1,a.$el&&a.$el.addClass("node-hidden")))},g.prototype._toggleSelected=function(a,b){return a?(this._setSelected(a,!a.state.selected,b),this):void 0},g.prototype._setSelected=function(b,c,d){if(!d||c!==b.state.selected){if(c)this._options.multiSelect||a.each(this._findNodes("true","state.selected"),a.proxy(function(b,c){this._setSelected(c,!1,a.extend(d,{unselecting:!0}))},this)),b.state.selected=!0,b.$el&&(b.$el.addClass("node-selected"),(b.selectedIcon||this._options.selectedIcon)&&b.$el.children("span.node-icon").removeClass(b.icon||this._options.nodeIcon).addClass(b.selectedIcon||this._options.selectedIcon)),this._triggerEvent("nodeSelected",b,d);else{if(this._options.preventUnselect&&d&&!d.unselecting&&1===this._findNodes("true","state.selected").length)return this;b.state.selected=!1,b.$el&&(b.$el.removeClass("node-selected"),(b.selectedIcon||this._options.selectedIcon)&&b.$el.children("span.node-icon").removeClass(b.selectedIcon||this._options.selectedIcon).addClass(b.icon||this._options.nodeIcon)),this._triggerEvent("nodeUnselected",b,d)}return this}},g.prototype._toggleChecked=function(a,b){a&&this._setChecked(a,!a.state.checked,b)},g.prototype._setChecked=function(a,b,c){c&&b===a.state.checked||(b?(a.state.checked=!0,a.$el&&(a.$el.addClass("node-checked"),a.$el.children("span.check-icon").removeClass(this._options.uncheckedIcon).addClass(this._options.checkedIcon)),this._triggerEvent("nodeChecked",a,c)):(a.state.checked=!1,a.$el&&(a.$el.removeClass("node-checked"),a.$el.children("span.check-icon").removeClass(this._options.checkedIcon).addClass(this._options.uncheckedIcon)),this._triggerEvent("nodeUnchecked",a,c)))},g.prototype._setDisabled=function(a,b,c){c&&b===a.state.disabled||(b?(a.state.disabled=!0,this._setSelected(a,!1,c),this._setChecked(a,!1,c),this._setExpanded(a,!1,c),a.$el&&a.$el.addClass("node-disabled"),this._triggerEvent("nodeDisabled",a,c)):(a.state.disabled=!1,a.$el&&a.$el.removeClass("node-disabled"),this._triggerEvent("nodeEnabled",a,c)))},g.prototype._setSearchResult=function(a,b,c){c&&b===a.searchResult||(b?(a.searchResult=!0,a.$el&&a.$el.addClass("node-result")):(a.searchResult=!1,a.$el&&a.$el.removeClass("node-result")))},g.prototype._render=function(){this._initialized||(this.$wrapper=a(this._template.tree),this.$element.empty().addClass(e).append(this.$wrapper),this._injectStyle(),this._initialized=!0);var b;a.each(this._orderedNodes,a.proxy(function(a,c){this._renderNode(c,b),b=c},this)),this._triggerEvent("rendered",this._orderedNodes,f.options)},g.prototype._renderNode=function(b,c){if(b){b.$el?b.$el.empty():b.$el=this._newNodeEl(b,c).addClass("node-"+this._elementId),b.$el.attr("data-nodeId",b.nodeId);for(var d=0;d '+this._buildStyle()+" ").appendTo("head")},g.prototype._buildStyle=function(){var b=".node-"+this._elementId+"{";if(this._options.color&&(b+="color:"+this._options.color+";"),this._options.backColor&&(b+="background-color:"+this._options.backColor+";"),this._options.showBorder?this._options.borderColor&&(b+="border:1px solid "+this._options.borderColor+";"):b+="border:none;",b+="}",this._options.onhoverColor&&(b+=".node-"+this._elementId+":not(.node-disabled):hover{background-color:"+this._options.onhoverColor+";}"),this._options.highlightSearchResults&&(this._options.searchResultColor||this._options.searchResultBackColor)){var c="";this._options.searchResultColor&&(c+="color:"+this._options.searchResultColor+";"),this._options.searchResultBackColor&&(c+="background-color:"+this._options.searchResultBackColor+";"),b+=".node-"+this._elementId+".node-result{"+c+"}",b+=".node-"+this._elementId+".node-result:hover{"+c+"}"}if(this._options.highlightSelected&&(this._options.selectedColor||this._options.selectedBackColor)){var c="";this._options.selectedColor&&(c+="color:"+this._options.selectedColor+";"),this._options.selectedBackColor&&(c+="background-color:"+this._options.selectedBackColor+";"),b+=".node-"+this._elementId+".node-selected{"+c+"}",b+=".node-"+this._elementId+".node-selected:hover{"+c+"}"}return a.each(this._orderedNodes,a.proxy(function(a,c){if(c.color||c.backColor){var d="";c.color&&(d+="color:"+c.color+";"),c.backColor&&(d+="background-color:"+c.backColor+";"),b+=".node-"+this._elementId+'[data-nodeId="'+c.nodeId+'"]{'+d+"}"}},this)),this._css+b},g.prototype._template={tree:'
      ',node:'
    • ',indent:'',icon:'',badge:''},g.prototype._css=".treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}",g.prototype.findNodes=function(a,b){return this._findNodes(a,b)},g.prototype.getNodes=function(){return this._orderedNodes},g.prototype.getParents=function(b){b instanceof Array||(b=[b]);var c=[];return a.each(b,a.proxy(function(a,b){var d=b.parentId?this._nodes[b.parentId]:!1;d&&c.push(d)},this)),c},g.prototype.getSiblings=function(b){b instanceof Array||(b=[b]);var c=[];return a.each(b,a.proxy(function(a,b){var d=this.getParents([b]),e=d[0]?d[0].nodes:this._tree;c=e.filter(function(a){return a.nodeId!==b.nodeId})},this)),a.map(c,function(a){return a})},g.prototype.getSelected=function(){return this._findNodes("true","state.selected")},g.prototype.getUnselected=function(){return this._findNodes("false","state.selected")},g.prototype.getExpanded=function(){return this._findNodes("true","state.expanded")},g.prototype.getCollapsed=function(){return this._findNodes("false","state.expanded")},g.prototype.getChecked=function(){return this._findNodes("true","state.checked")},g.prototype.getUnchecked=function(){return this._findNodes("false","state.checked")},g.prototype.getDisabled=function(){return this._findNodes("true","state.disabled")},g.prototype.getEnabled=function(){return this._findNodes("false","state.disabled")},g.prototype.addNode=function(b,c,d,e){b instanceof Array||(b=[b]),c instanceof Array&&(c=c[0]),e=a.extend({},f.options,e);var g;g=c&&c.nodes?c.nodes:c?c.nodes=[]:this._tree,a.each(b,a.proxy(function(a,b){var c="number"==typeof d?d+a:g.length+1;g.splice(c,0,b)},this)),this._setInitialStates({nodes:this._tree},0).done(a.proxy(function(){c&&!c.state.expanded&&this._setExpanded(c,!0,e),this._render()},this))},g.prototype.addNodeAfter=function(b,c,d){b instanceof Array||(b=[b]),c instanceof Array&&(c=c[0]),d=a.extend({},f.options,d),this.addNode(b,this.getParents(c)[0],c.index+1,d)},g.prototype.addNodeBefore=function(b,c,d){b instanceof Array||(b=[b]),c instanceof Array&&(c=c[0]),d=a.extend({},f.options,d),this.addNode(b,this.getParents(c)[0],c.index,d)},g.prototype.removeNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c);var d,e;a.each(b,a.proxy(function(a,b){e=this._nodes[b.parentId],d=e?e.nodes:this._tree,d.splice(b.index,1),this._removeNodeEl(b)},this)),this._setInitialStates({nodes:this._tree},0).done(this._render.bind(this))},g.prototype.updateNode=function(b,c,d){b instanceof Array&&(b=b[0]),d=a.extend({},f.options,d);var e,g=this._nodes[b.parentId];e=g?g.nodes:this._tree,e.splice(b.index,1,c),this._removeNodeEl(b),this._setInitialStates({nodes:this._tree},0).done(this._render.bind(this))},g.prototype.selectNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setSelected(b,!0,c)},this))},g.prototype.unselectNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setSelected(b,!1,c)},this))},g.prototype.toggleNodeSelected=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._toggleSelected(b,c)},this))},g.prototype.collapseAll=function(b){b=a.extend({},f.options,b),b.levels=b.levels||999,this.collapseNode(this._tree,b)},g.prototype.collapseNode=function(b,c){c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setExpanded(b,!1,c)},this))},g.prototype.expandAll=function(b){b=a.extend({},f.options,b),b.levels=b.levels||999,this.expandNode(this._tree,b)},g.prototype.expandNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setExpanded(b,!0,c),b.nodes&&this._expandLevels(b.nodes,c.levels-1,c)},this))},g.prototype._expandLevels=function(b,c,d){b instanceof Array||(b=[b]),d=a.extend({},f.options,d),a.each(b,a.proxy(function(a,b){this._setExpanded(b,c>0,d),b.nodes&&this._expandLevels(b.nodes,c-1,d)},this))},g.prototype.revealNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){for(var d,e=b;d=this.getParents([e])[0];)e=d,this._setExpanded(e,!0,c)},this))},g.prototype.toggleNodeExpanded=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._toggleExpanded(b,c)},this))},g.prototype.checkAll=function(b){b=a.extend({},f.options,b);var c=this._findNodes("false","state.checked");a.each(c,a.proxy(function(a,c){this._setChecked(c,!0,b)},this))},g.prototype.checkNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setChecked(b,!0,c)},this))},g.prototype.uncheckAll=function(b){b=a.extend({},f.options,b);var c=this._findNodes("true","state.checked");a.each(c,a.proxy(function(a,c){this._setChecked(c,!1,b)},this))},g.prototype.uncheckNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setChecked(b,!1,c)},this))},g.prototype.toggleNodeChecked=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._toggleChecked(b,c)},this))},g.prototype.disableAll=function(b){b=a.extend({},f.options,b);var c=this._findNodes("false","state.disabled");a.each(c,a.proxy(function(a,c){this._setDisabled(c,!0,b)},this))},g.prototype.disableNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setDisabled(b,!0,c)},this))},g.prototype.enableAll=function(b){b=a.extend({},f.options,b);var c=this._findNodes("true","state.disabled");a.each(c,a.proxy(function(a,c){this._setDisabled(c,!1,b)},this))},g.prototype.enableNode=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setDisabled(b,!1,c)},this))},g.prototype.toggleNodeDisabled=function(b,c){b instanceof Array||(b=[b]),c=a.extend({},f.options,c),a.each(b,a.proxy(function(a,b){this._setDisabled(b,!b.state.disabled,c)},this))},g.prototype.search=function(b,c){c=a.extend({},f.searchOptions,c);var d=this._getSearchResults(),e=[];if(b&&b.length>0){c.exactMatch&&(b="^"+b+"$");var g="g";c.ignoreCase&&(g+="i"),e=this._findNodes(b,"text",g)}return a.each(this._diffArray(e,d),a.proxy(function(a,b){this._setSearchResult(b,!1,c)},this)),a.each(this._diffArray(d,e),a.proxy(function(a,b){this._setSearchResult(b,!0,c)},this)),e&&c.revealResults&&this.revealNode(e),this._triggerEvent("searchComplete",e,c),e},g.prototype.clearSearch=function(b){b=a.extend({},{render:!0},b);var c=a.each(this._getSearchResults(),a.proxy(function(a,c){this._setSearchResult(c,!1,b)},this));this._triggerEvent("searchCleared",c,b)},g.prototype._getSearchResults=function(){return this._findNodes("true","searchResult")},g.prototype._diffArray=function(b,c){var d=[];return a.grep(c,function(c){-1===a.inArray(c,b)&&d.push(c)}),d},g.prototype._findNodes=function(b,c,d){return c=c||"text",d=d||"g",a.grep(this._orderedNodes,a.proxy(function(a){var e=this._getNodeValue(a,c);return"string"==typeof e?e.match(new RegExp(b,d)):void 0},this))},g.prototype._getNodeValue=function(a,b){var c=b.indexOf(".");if(c>0){var e=a[b.substring(0,c)],f=b.substring(c+1,b.length);return this._getNodeValue(e,f)}return a.hasOwnProperty(b)?a[b].toString():d};var h=function(a){b.console&&b.console.error(a)};a.fn[e]=function(b,c){var d;return this.each(function(){var f=a.data(this,e);"string"==typeof b?f?a.isFunction(f[b])&&"_"!==b.charAt(0)?(c instanceof Array||(c=[c]),d=f[b].apply(f,c)):h("No such method : "+b):h("Not initialized, can not call method : "+b):"boolean"==typeof b?d=f:a.data(this,e,new g(this,a.extend(!0,{},b)))}),d||this}}(jQuery,window,document); \ No newline at end of file diff --git a/package.json b/package.json index 31ef86ddb..d52aa790b 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,9 @@ "bugs": { "url": "https://github.com/jonmiles/bootstrap-treeview/issues" }, - "licenses": [ - { - "type": "Apache", - "url": "https://github.com/jonmiles/bootstrap-treeview/blob/master/LICENSE" - } - ], - "main": [ - "dist/bootstrap-treeview.min.js", - "dist/bootstrap-treeview.min.css" - ], + "license": "Apache-2.0", + "main": "src/bootstrap-treeview.js", "scripts": { - "install": "bower install", "start": "node app", "test": "grunt test" }, @@ -32,18 +23,19 @@ "node": ">= 0.10.0" }, "dependencies": { - "express": "3.4.x", - "ejs": "2.2.x", - "phantomjs": "1.9.x" + "bootstrap": "3.3.x", + "jquery": "2.1.x" }, "devDependencies": { "bower": "1.3.x", - "grunt": "0.4.x", - "grunt-contrib-uglify": "0.7.x", - "grunt-contrib-cssmin": "0.12.x", - "grunt-contrib-qunit": "0.5.x", - "grunt-contrib-watch": "0.6.x", - "grunt-contrib-copy": "0.7.x" + "express": "3.4.x", + "grunt": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-cssmin": "^1.0.1", + "grunt-contrib-qunit": "^1.2.0", + "grunt-contrib-uglify": "^1.0.1", + "grunt-contrib-watch": "^1.0.0", + "phantomjs": "^2.1.7" }, "keywords": [ "twitter", diff --git a/public/css/bootstrap-treeview.css b/public/css/bootstrap-treeview.css index 23c6cf066..bccab55ab 100644 --- a/public/css/bootstrap-treeview.css +++ b/public/css/bootstrap-treeview.css @@ -1,9 +1,9 @@ /* ========================================================= - * bootstrap-treeview.css v1.2.0 + * bootstrap-treeview.css v2.0.0 * ========================================================= - * Copyright 2013 Jonathan Miles + * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -34,4 +34,8 @@ .treeview .node-disabled { color: silver; cursor: not-allowed; -} \ No newline at end of file +} + +.treeview .node-hidden { + display: none; +} diff --git a/public/data.json b/public/data.json new file mode 100644 index 000000000..ff16499ae --- /dev/null +++ b/public/data.json @@ -0,0 +1,42 @@ +[ + { + "text": "Parent 1", + "tags": [4], + "nodes": [ + { + "text": "Child 1", + "tags": [2], + "nodes": [ + { + "text": "Grandchild 1", + "tags": [0] + }, + { + "text": "Grandchild 2", + "tags": [0] + } + ] + }, + { + "text": "Child 2", + "tags": [0] + } + ] + }, + { + "text": "Parent 2", + "tags": [0] + }, + { + "text": "Parent 3", + "tags": [0] + }, + { + "text": "Parent 4", + "tags": [0] + }, + { + "text": "Parent 5", + "tags": [0] + } +] diff --git a/public/example-dom.html b/public/example-dom.html index 8fe5f75b6..f51e239dd 100644 --- a/public/example-dom.html +++ b/public/example-dom.html @@ -2,7 +2,7 @@ Bootstrap Tree View - + @@ -16,12 +16,12 @@

      Bootstrap Tree View - DOM Tree

      - + + diff --git a/public/js/bootstrap-treeview.js b/public/js/bootstrap-treeview.js index 7a82a2eeb..820a1f941 100644 --- a/public/js/bootstrap-treeview.js +++ b/public/js/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.2.0 + * bootstrap-treeview.js v2.0.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -41,16 +41,15 @@ checkedIcon: 'glyphicon glyphicon-check', uncheckedIcon: 'glyphicon glyphicon-unchecked', - color: undefined, // '#000000', - backColor: undefined, // '#FFFFFF', - borderColor: undefined, // '#dddddd', + color: undefined, + backColor: undefined, + borderColor: undefined, onhoverColor: '#F5F5F5', selectedColor: '#FFFFFF', selectedBackColor: '#428bca', searchResultColor: '#D9534F', - searchResultBackColor: undefined, //'#FFFFFF', + searchResultBackColor: undefined, - enableLinks: false, highlightSelected: true, highlightSearchResults: true, showBorder: true, @@ -58,8 +57,16 @@ showCheckbox: false, showTags: false, multiSelect: false, + preventUnselect: false, // Event handlers + onLoading: undefined, + onLoadingFailed: undefined, + onInitialized: undefined, + onNodeRendered: undefined, + onRendered: undefined, + onDestroyed: undefined, + onNodeChecked: undefined, onNodeCollapsed: undefined, onNodeDisabled: undefined, @@ -68,6 +75,7 @@ onNodeSelected: undefined, onNodeUnchecked: undefined, onNodeUnselected: undefined, + onSearchComplete: undefined, onSearchCleared: undefined }; @@ -83,26 +91,32 @@ revealResults: true }; - var Tree = function (element, options) { + _default.dataUrl = { + method: 'GET', + dataType: 'json', + cache: false + }; + var Tree = function (element, options) { this.$element = $(element); - this.elementId = element.id; - this.styleId = this.elementId + '-style'; + this._elementId = element.id; + this._styleId = this._elementId + '-style'; - this.init(options); + this._init(options); return { // Options (public access) - options: this.options, + options: this._options, // Initialize / destroy methods - init: $.proxy(this.init, this), - remove: $.proxy(this.remove, this), + init: $.proxy(this._init, this), + remove: $.proxy(this._remove, this), - // Get methods - getNode: $.proxy(this.getNode, this), - getParent: $.proxy(this.getParent, this), + // Query methods + findNodes: $.proxy(this.findNodes, this), + getNodes: $.proxy(this.getNodes, this), // todo document + test + getParents: $.proxy(this.getParents, this), getSiblings: $.proxy(this.getSiblings, this), getSelected: $.proxy(this.getSelected, this), getUnselected: $.proxy(this.getUnselected, this), @@ -113,6 +127,13 @@ getDisabled: $.proxy(this.getDisabled, this), getEnabled: $.proxy(this.getEnabled, this), + // Tree manipulation methods + addNode: $.proxy(this.addNode, this), + addNodeAfter: $.proxy(this.addNodeAfter, this), + addNodeBefore: $.proxy(this.addNodeBefore, this), + removeNode: $.proxy(this.removeNode, this), + updateNode: $.proxy(this.updateNode, this), + // Select methods selectNode: $.proxy(this.selectNode, this), unselectNode: $.proxy(this.unselectNode, this), @@ -126,7 +147,7 @@ toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), revealNode: $.proxy(this.revealNode, this), - // Expand / collapse methods + // Check / uncheck methods checkAll: $.proxy(this.checkAll, this), checkNode: $.proxy(this.checkNode, this), uncheckAll: $.proxy(this.uncheckAll, this), @@ -146,48 +167,87 @@ }; }; - Tree.prototype.init = function (options) { - - this.tree = []; - this.nodes = []; + Tree.prototype._init = function (options) { + this._tree = []; + this._initialized = false; + + this._options = $.extend({}, _default.settings, options); + + this._destroy(); + this._subscribeEvents(); + + this._triggerEvent('loading', null, _default.options); + this._load(options) + .then($.proxy(function (data) { + // load done + return this._tree = $.extend(true, [], data); + }, this), $.proxy(function (error) { + // load fail + this._triggerEvent('loadingFailed', error, _default.options); + }, this)) + .then($.proxy(function (treeData) { + // initialize data + return this._setInitialStates({ nodes: treeData }, 0); + }, this)) + .then($.proxy(function () { + // render to DOM + this._render(); + }, this)); + }; + Tree.prototype._load = function (options) { + var done = new $.Deferred(); if (options.data) { - if (typeof options.data === 'string') { - options.data = $.parseJSON(options.data); - } - this.tree = $.extend(true, [], options.data); - delete options.data; + this._loadLocalData(options, done); + } else if (options.dataUrl) { + this._loadRemoteData(options, done); } - this.options = $.extend({}, _default.settings, options); + return done.promise(); + }; - this.destroy(); - this.subscribeEvents(); - this.setInitialStates({ nodes: this.tree }, 0); - this.render(); + Tree.prototype._loadRemoteData = function (options, done) { + $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl)) + .done(function (data) { + done.resolve(data); + }) + .fail(function (xhr, status, error) { + done.reject(error); + }); }; - Tree.prototype.remove = function () { - this.destroy(); - $.removeData(this, pluginName); - $('#' + this.styleId).remove(); + Tree.prototype._loadLocalData = function (options, done) { + done.resolve((typeof options.data === 'string') ? + $.parseJSON(options.data) : + $.extend(true, [], options.data)); }; - Tree.prototype.destroy = function () { + Tree.prototype._remove = function () { + this._destroy(); + $.removeData(this, pluginName); + $('#' + this._styleId).remove(); + }; - if (!this.initialized) return; + Tree.prototype._destroy = function () { + if (!this._initialized) return; + this._initialized = false; - this.$wrapper.remove(); - this.$wrapper = null; + this._triggerEvent('destroyed', null, _default.options); // Switch off events - this.unsubscribeEvents(); + this._unsubscribeEvents(); - // Reset this.initialized flag - this.initialized = false; + // Tear down + this.$wrapper.remove(); + this.$wrapper = null; }; - Tree.prototype.unsubscribeEvents = function () { - + Tree.prototype._unsubscribeEvents = function () { + this.$element.off('loading'); + this.$element.off('loadingFailed'); + this.$element.off('initialized'); + this.$element.off('nodeRendered'); + this.$element.off('rendered'); + this.$element.off('destroyed'); this.$element.off('click'); this.$element.off('nodeChecked'); this.$element.off('nodeCollapsed'); @@ -201,70 +261,118 @@ this.$element.off('searchCleared'); }; - Tree.prototype.subscribeEvents = function () { + Tree.prototype._subscribeEvents = function () { + this._unsubscribeEvents(); + + if (typeof (this._options.onLoading) === 'function') { + this.$element.on('loading', this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.$element.on('loadingFailed', this._options.onLoadingFailed); + } - this.unsubscribeEvents(); + if (typeof (this._options.onInitialized) === 'function') { + this.$element.on('initialized', this._options.onInitialized); + } - this.$element.on('click', $.proxy(this.clickHandler, this)); + if (typeof (this._options.onNodeRendered) === 'function') { + this.$element.on('nodeRendered', this._options.onNodeRendered); + } - if (typeof (this.options.onNodeChecked) === 'function') { - this.$element.on('nodeChecked', this.options.onNodeChecked); + if (typeof (this._options.onRendered) === 'function') { + this.$element.on('rendered', this._options.onRendered); } - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); + if (typeof (this._options.onDestroyed) === 'function') { + this.$element.on('destroyed', this._options.onDestroyed); } - if (typeof (this.options.onNodeDisabled) === 'function') { - this.$element.on('nodeDisabled', this.options.onNodeDisabled); + this.$element.on('click', $.proxy(this._clickHandler, this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.$element.on('nodeChecked', this._options.onNodeChecked); } - if (typeof (this.options.onNodeEnabled) === 'function') { - this.$element.on('nodeEnabled', this.options.onNodeEnabled); + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.$element.on('nodeCollapsed', this._options.onNodeCollapsed); } - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.on('nodeExpanded', this.options.onNodeExpanded); + if (typeof (this._options.onNodeDisabled) === 'function') { + this.$element.on('nodeDisabled', this._options.onNodeDisabled); } - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.on('nodeSelected', this.options.onNodeSelected); + if (typeof (this._options.onNodeEnabled) === 'function') { + this.$element.on('nodeEnabled', this._options.onNodeEnabled); } - if (typeof (this.options.onNodeUnchecked) === 'function') { - this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); + if (typeof (this._options.onNodeExpanded) === 'function') { + this.$element.on('nodeExpanded', this._options.onNodeExpanded); } - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.on('nodeUnselected', this.options.onNodeUnselected); + if (typeof (this._options.onNodeSelected) === 'function') { + this.$element.on('nodeSelected', this._options.onNodeSelected); } - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.on('searchComplete', this.options.onSearchComplete); + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.$element.on('nodeUnchecked', this._options.onNodeUnchecked); } - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.on('searchCleared', this.options.onSearchCleared); + if (typeof (this._options.onNodeUnselected) === 'function') { + this.$element.on('nodeUnselected', this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.$element.on('searchComplete', this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.$element.on('searchCleared', this._options.onSearchCleared); } }; + Tree.prototype._triggerEvent = function (event, data, options) { + if (options && !options.silent) { + this.$element.trigger(event, $.extend(true, {}, data)); + } + } + /* Recurse the tree structure and ensure all nodes have valid initial states. User defined states will be preserved. For performance we also take this opportunity to - index nodes in a flattened structure + index nodes in a flattened ordered structure */ - Tree.prototype.setInitialStates = function (node, level) { + Tree.prototype._setInitialStates = function (node, level) { + this._nodes = {}; + return $.when.apply(this, this._setInitialState(node, level)) + .done($.proxy(function () { + this._orderedNodes = this._sortNodes(); + this._triggerEvent('initialized', this._orderedNodes, _default.options); + return; + }, this)); + }; + Tree.prototype._setInitialState = function (node, level, done) { if (!node.nodes) return; level += 1; + done = done || []; var parent = node; - var _this = this; - $.each(node.nodes, function checkStates(index, node) { + $.each(node.nodes, $.proxy(function (index, node) { + var deferred = new $.Deferred(); + done.push(deferred.promise()); + + // level : hierarchical tree level, starts at 1 + node.level = level; - // nodeId : unique, incremental identifier - node.nodeId = _this.nodes.length; + // index : relative to siblings + node.index = index; + + // nodeId : unique, hierarchical identifier + node.nodeId = (parent && parent.nodeId) ? + parent.nodeId + '.' + node.index : + (level - 1) + '.' + node.index; // parentId : transversing up the tree node.parentId = parent.nodeId; @@ -290,7 +398,7 @@ // set expanded state; if not provided based on levels if (!node.state.hasOwnProperty('expanded')) { if (!node.state.disabled && - (level < _this.options.levels) && + (level < this._options.levels) && (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } @@ -304,431 +412,616 @@ node.state.selected = false; } - // index nodes in a flattened structure for use later - _this.nodes.push(node); + // set visible state; based parent state plus levels + if ((parent && parent.state && parent.state.expanded) || + (level <= this._options.levels)) { + node.state.visible = true; + } + else { + node.state.visible = false; + } - // recurse child nodes and transverse the tree + // recurse child nodes and transverse the tree, depth-first if (node.nodes) { - _this.setInitialStates(node, level); + if (node.nodes.length > 0) { + this._setInitialState(node, level, done); + } + else { + delete node.nodes; + } } - }); + + // add / update indexed collection + this._nodes[node.nodeId] = node; + + // mark task as complete + deferred.resolve(); + }, this)); + + return done; }; - Tree.prototype.clickHandler = function (event) { + Tree.prototype._sortNodes = function () { + return $.map(Object.keys(this._nodes).sort(), $.proxy(function (value, index) { + return this._nodes[value]; + }, this)); + }; - if (!this.options.enableLinks) event.preventDefault(); + Tree.prototype._clickHandler = function (event) { var target = $(event.target); - var node = this.findNode(target); + var node = this.targetNode(target); if (!node || node.state.disabled) return; - + var classList = target.attr('class') ? target.attr('class').split(' ') : []; if ((classList.indexOf('expand-icon') !== -1)) { - - this.toggleExpandedState(node, _default.options); - this.render(); + this._toggleExpanded(node, $.extend({}, _default.options)); } else if ((classList.indexOf('check-icon') !== -1)) { - - this.toggleCheckedState(node, _default.options); - this.render(); + this._toggleChecked(node, $.extend({}, _default.options)); } else { - if (node.selectable) { - this.toggleSelectedState(node, _default.options); + this._toggleSelected(node, $.extend({}, _default.options)); } else { - this.toggleExpandedState(node, _default.options); + this._toggleExpanded(node, $.extend({}, _default.options)); } - - this.render(); } }; // Looks up the DOM for the closest parent list item to retrieve the // data attribute nodeid, which is used to lookup the node in the flattened structure. - Tree.prototype.findNode = function (target) { - - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); - var node = this.nodes[nodeId]; - + Tree.prototype.targetNode = function (target) { + var nodeId = target.closest('li.list-group-item').attr('data-nodeId'); + var node = this._nodes[nodeId]; if (!node) { console.log('Error: node does not exist'); } return node; }; - Tree.prototype.toggleExpandedState = function (node, options) { + Tree.prototype._toggleExpanded = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, options); + this._setExpanded(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, options) { + Tree.prototype._setExpanded = function (node, state, options) { - if (state === node.state.expanded) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.expanded) return; if (state && node.nodes) { - // Expand a node + // Set node state node.state.expanded = true; - if (!options.silent) { - this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .addClass(this._options.collapseIcon); + } + + // Expand children + if (node.nodes && options) { + $.each(node.nodes, $.proxy(function (index, node) { + this._setVisible(node, true, options); + }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeExpanded', node, options); } else if (!state) { - // Collapse a node + // Set node state node.state.expanded = false; - if (!options.silent) { - this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.collapseIcon) + .addClass(this._options.expandIcon); } - // Collapse child nodes - if (node.nodes && !options.ignoreChildren) { + // Collapse children + if (node.nodes && options) { $.each(node.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, options); + this._setVisible(node, false, options); + this._setExpanded(node, false, options); }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeCollapsed', node, options); } }; - Tree.prototype.toggleSelectedState = function (node, options) { + Tree.prototype._setVisible = function (node, state, options) { + + if (options && state === node.state.visible) return; + + if (state) { + + // Set node state + node.state.visible = true; + + // Set element + if (node.$el) { + node.$el.removeClass('node-hidden'); + } + } + else { + + // Set node state to unchecked + node.state.visible = false; + + // Set element + if (node.$el) { + node.$el.addClass('node-hidden'); + } + } + }; + + Tree.prototype._toggleSelected = function (node, options) { if (!node) return; - this.setSelectedState(node, !node.state.selected, options); + this._setSelected(node, !node.state.selected, options); + return this; }; - Tree.prototype.setSelectedState = function (node, state, options) { + Tree.prototype._setSelected = function (node, state, options) { - if (state === node.state.selected) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && (state === node.state.selected)) return; if (state) { // If multiSelect false, unselect previously selected - if (!this.options.multiSelect) { - $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, options); + if (!this._options.multiSelect) { + $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) { + this._setSelected(node, false, $.extend(options, {unselecting: true})); }, this)); } - // Continue selecting node + // Set node state node.state.selected = true; - if (!options.silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.addClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.icon || this._options.nodeIcon) + .addClass(node.selectedIcon || this._options.selectedIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeSelected', node, options); } else { - // Unselect node + // If preventUnselect true + only one remaining selection, disable unselect + if (this._options.preventUnselect && + (options && !options.unselecting) && + (this._findNodes('true', 'state.selected').length === 1)) { + return this; + } + + // Set node state node.state.selected = false; - if (!options.silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.selectedIcon || this._options.selectedIcon) + .addClass(node.icon || this._options.nodeIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeUnselected', node, options); } + + return this; }; - Tree.prototype.toggleCheckedState = function (node, options) { + Tree.prototype._toggleChecked = function (node, options) { if (!node) return; - this.setCheckedState(node, !node.state.checked, options); + this._setChecked(node, !node.state.checked, options); }; - Tree.prototype.setCheckedState = function (node, state, options) { + Tree.prototype._setChecked = function (node, state, options) { - if (state === node.state.checked) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.checked) return; if (state) { - // Check node + // Set node state node.state.checked = true; - if (!options.silent) { - this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .addClass(this._options.checkedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); } else { - // Uncheck node + // Set node state to unchecked node.state.checked = false; - if (!options.silent) { - this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.checkedIcon) + .addClass(this._options.uncheckedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeUnchecked', node, options); } }; - Tree.prototype.setDisabledState = function (node, state, options) { + Tree.prototype._setDisabled = function (node, state, options) { - if (state === node.state.disabled) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.disabled) return; if (state) { - // Disable node + // Set node state to disabled node.state.disabled = true; // Disable all other states - this.setExpandedState(node, false, options); - this.setSelectedState(node, false, options); - this.setCheckedState(node, false, options); + this._setSelected(node, false, options); + this._setChecked(node, false, options); + this._setExpanded(node, false, options); - if (!options.silent) { - this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeDisabled', node, options); } else { - // Enabled node + // Set node state to enabled node.state.disabled = false; - if (!options.silent) { - this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeEnabled', node, options); } }; - Tree.prototype.render = function () { + Tree.prototype._setSearchResult = function (node, state, options) { + if (options && state === node.searchResult) return; - if (!this.initialized) { - - // Setup first time only components - this.$element.addClass(pluginName); - this.$wrapper = $(this.template.list); + if (state) { - this.injectStyle(); + node.searchResult = true; - this.initialized = true; + if (node.$el) { + node.$el.addClass('node-result'); + } } + else { - this.$element.empty().append(this.$wrapper.empty()); + node.searchResult = false; - // Build tree - this.buildTree(this.tree, 0); + if (node.$el) { + node.$el.removeClass('node-result'); + } + } }; - // Starting from the root node, and recursing down the - // structure we build the tree one node at a time - Tree.prototype.buildTree = function (nodes, level) { + Tree.prototype._render = function () { + if (!this._initialized) { - if (!nodes) return; - level += 1; + // Setup first time only components + this.$wrapper = $(this._template.tree); + this.$element.empty() + .addClass(pluginName) + .append(this.$wrapper); - var _this = this; - $.each(nodes, function addNodes(id, node) { - - var treeItem = $(_this.template.item) - .addClass('node-' + _this.elementId) - .addClass(node.state.checked ? 'node-checked' : '') - .addClass(node.state.disabled ? 'node-disabled': '') - .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') - .attr('data-nodeid', node.nodeId) - .attr('style', _this.buildStyleOverride(node)); - - // Add indent/spacer to mimic tree structure - for (var i = 0; i < (level - 1); i++) { - treeItem.append(_this.template.indent); - } + this._injectStyle(); - // Add expand, collapse or empty spacer icons - var classList = []; - if (node.nodes) { - classList.push('expand-icon'); - if (node.state.expanded) { - classList.push(_this.options.collapseIcon); - } - else { - classList.push(_this.options.expandIcon); - } - } - else { - classList.push(_this.options.emptyIcon); - } + this._initialized = true; + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); + var previousNode; + $.each(this._orderedNodes, $.proxy(function (id, node) { + this._renderNode(node, previousNode); + previousNode = node; + }, this)); + this._triggerEvent('rendered', this._orderedNodes, _default.options); + }; - // Add node icon - if (_this.options.showIcon) { - - var classList = ['node-icon']; + Tree.prototype._renderNode = function (node, previousNode) { + if (!node) return; - classList.push(node.icon || _this.options.nodeIcon); - if (node.state.selected) { - classList.pop(); - classList.push(node.selectedIcon || _this.options.selectedIcon || - node.icon || _this.options.nodeIcon); - } + if (!node.$el) { + node.$el = this._newNodeEl(node, previousNode) + .addClass('node-' + this._elementId); + } + else { + node.$el.empty(); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Set / update nodeid; it can change as a result of addNode etc. + node.$el.attr('data-nodeId', node.nodeId); - // Add check / unchecked icon - if (_this.options.showCheckbox) { + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (node.level - 1); i++) { + node.$el.append(this._template.indent); + } - var classList = ['check-icon']; - if (node.state.checked) { - classList.push(_this.options.checkedIcon); - } - else { - classList.push(_this.options.uncheckedIcon); - } + // Add expand / collapse or empty spacer icons + node.$el + .append($(this._template.icon) + .addClass(node.nodes ? 'expand-icon' : this._options.emptyIcon) + ); + + // Add node icon + if (this._options.showIcon) { + node.$el + .append($(this._template.icon) + .addClass('node-icon') + .addClass(node.icon || this._options.nodeIcon) + ); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Add checkable icon + if (this._options.showCheckbox) { + node.$el + .append($(this._template.icon) + .addClass('check-icon') + ); + } - // Add text - if (_this.options.enableLinks) { - // Add hyperlink - treeItem - .append($(_this.template.link) - .attr('href', node.href) - .append(node.text) - ); - } - else { - // otherwise just text - treeItem - .append(node.text); - } + // Add text + node.$el.append(node.text); - // Add tags as badges - if (_this.options.showTags && node.tags) { - $.each(node.tags, function addTag(id, tag) { - treeItem - .append($(_this.template.badge) - .append(tag) - ); - }); - } + // Add tags as badges + if (this._options.showTags && node.tags) { + $.each(node.tags, $.proxy(function addTag(id, tag) { + node.$el + .append($(this._template.badge) + .append(tag) + ); + }, this)); + } - // Add item to the tree - _this.$wrapper.append(treeItem); + // Set various node states + this._setSelected(node, node.state.selected); + this._setChecked(node, node.state.checked); + this._setSearchResult(node, node.searchResult); + this._setExpanded(node, node.state.expanded); + this._setDisabled(node, node.state.disabled); + this._setVisible(node, node.state.visible); + + // Trigger nodeRendered event + this._triggerEvent('nodeRendered', node, _default.options); + }; + + // Creates a new node element from template and + // ensures the template is inserted at the correct position + Tree.prototype._newNodeEl = function (node, previousNode) { + var $el = $(this._template.node); + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + this.$wrapper.children() + .eq(previousNode.$el.index()) + .after($el); + } else { + // we use prepend instead of append, + // to cater for root inserts i.e. nodeId 0.0 + this.$wrapper.prepend($el); + } - // Recursively add child ndoes - if (node.nodes && node.state.expanded && !node.state.disabled) { - return _this.buildTree(node.nodes, level); - } - }); + return $el; }; - // Define any node level style override for - // 1. selectedNode - // 2. node|data assigned color overrides - Tree.prototype.buildStyleOverride = function (node) { - - if (node.state.disabled) return ''; - - var color = node.color; - var backColor = node.backColor; + // Recursively remove node elements from DOM + Tree.prototype._removeNodeEl = function (node) { + if (!node) return; - if (this.options.highlightSelected && node.state.selected) { - if (this.options.selectedColor) { - color = this.options.selectedColor; - } - if (this.options.selectedBackColor) { - backColor = this.options.selectedBackColor; - } + if (node.nodes) { + $.each(node.nodes, $.proxy(function (index, node) { + this._removeNodeEl(node); + }, this)); } + node.$el.remove(); + }; - if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { - if (this.options.searchResultColor) { - color = this.options.searchResultColor; - } - if (this.options.searchResultBackColor) { - backColor = this.options.searchResultBackColor; - } - } + // Expand node, rendering it's immediate children + Tree.prototype._expandNode = function (node) { + if (!node.nodes) return; - return 'color:' + color + - ';background-color:' + backColor + ';'; + $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) { + childNode.level = node.level + 1; + this._renderNode(childNode, node.$el); + }, this)); }; // Add inline style into head - Tree.prototype.injectStyle = function () { - - if (this.options.injectStyle && !document.getElementById(this.styleId)) { - $('').appendTo('head'); + Tree.prototype._injectStyle = function () { + if (this._options.injectStyle && !document.getElementById(this._styleId)) { + $('').appendTo('head'); } }; // Construct trees style based on user options - Tree.prototype.buildStyle = function () { - - var style = '.node-' + this.elementId + '{'; + Tree.prototype._buildStyle = function () { + var style = '.node-' + this._elementId + '{'; - if (this.options.color) { - style += 'color:' + this.options.color + ';'; + // Basic bootstrap style overrides + if (this._options.color) { + style += 'color:' + this._options.color + ';'; } - if (this.options.backColor) { - style += 'background-color:' + this.options.backColor + ';'; + if (this._options.backColor) { + style += 'background-color:' + this._options.backColor + ';'; } - if (!this.options.showBorder) { + if (!this._options.showBorder) { style += 'border:none;'; } - else if (this.options.borderColor) { - style += 'border:1px solid ' + this.options.borderColor + ';'; + else if (this._options.borderColor) { + style += 'border:1px solid ' + this._options.borderColor + ';'; } style += '}'; - if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + if (this._options.onhoverColor) { + style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this._options.onhoverColor + ';' + '}'; } - return this.css + style; + // Style search results + if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { + + var innerStyle = '' + if (this._options.searchResultColor) { + innerStyle += 'color:' + this._options.searchResultColor + ';'; + } + if (this._options.searchResultBackColor) { + innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; + } + + // Style selected nodes + if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { + + var innerStyle = '' + if (this._options.selectedColor) { + innerStyle += 'color:' + this._options.selectedColor + ';'; + } + if (this._options.selectedBackColor) { + innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; + } + + // Node level style overrides + $.each(this._orderedNodes, $.proxy(function (index, node) { + if (node.color || node.backColor) { + var innerStyle = ''; + if (node.color) { + innerStyle += 'color:' + node.color + ';'; + } + if (node.backColor) { + innerStyle += 'background-color:' + node.backColor + ';'; + } + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; + } + }, this)); + + return this._css + style; }; - Tree.prototype.template = { - list: '
        ', - item: '
      • ', + Tree.prototype._template = { + tree: '
          ', + node: '
        • ', indent: '', icon: '', - link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + Tree.prototype._css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + Returns an array of matching node objects. + @param {String} pattern - A pattern to match against a given field + @return {String} field - Field to query pattern against + */ + Tree.prototype.findNodes = function (pattern, field) { + return this._findNodes(pattern, field); + }; /** - Returns a single node object that matches the given node id. - @param {Number} nodeId - A node's unique identifier - @return {Object} node - Matching node + Returns an ordered aarray of node objects. + @return {Array} nodes - An array of all nodes */ - Tree.prototype.getNode = function (nodeId) { - return this.nodes[nodeId]; + Tree.prototype.getNodes = function () { + return this._orderedNodes; }; /** - Returns the parent node of a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Object} node - The parent node + Returns parent nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of parent nodes */ - Tree.prototype.getParent = function (identifier) { - var node = this.identifyNode(identifier); - return this.nodes[node.parentId]; + Tree.prototype.getParents = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var parentNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node.parentId ? this._nodes[node.parentId] : false; + if (parentNode) { + parentNodes.push(parentNode); + } + }, this)); + return parentNodes; }; /** - Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Array} nodes - Sibling nodes + Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of sibling nodes */ - Tree.prototype.getSiblings = function (identifier) { - var node = this.identifyNode(identifier); - var parent = this.getParent(node); - var nodes = parent ? parent.nodes : this.tree; - return nodes.filter(function (obj) { + Tree.prototype.getSiblings = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var siblingNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parent = this.getParents([node]); + var nodes = parent[0] ? parent[0].nodes : this._tree; + siblingNodes = nodes.filter(function (obj) { return obj.nodeId !== node.nodeId; }); + }, this)); + + // flatten possible nested array before returning + return $.map(siblingNodes, function (obj) { + return obj; + }); }; /** @@ -736,7 +1029,7 @@ @returns {Array} nodes - Selected nodes */ Tree.prototype.getSelected = function () { - return this.findNodes('true', 'g', 'state.selected'); + return this._findNodes('true', 'state.selected'); }; /** @@ -744,7 +1037,7 @@ @returns {Array} nodes - Unselected nodes */ Tree.prototype.getUnselected = function () { - return this.findNodes('false', 'g', 'state.selected'); + return this._findNodes('false', 'state.selected'); }; /** @@ -752,7 +1045,7 @@ @returns {Array} nodes - Expanded nodes */ Tree.prototype.getExpanded = function () { - return this.findNodes('true', 'g', 'state.expanded'); + return this._findNodes('true', 'state.expanded'); }; /** @@ -760,7 +1053,7 @@ @returns {Array} nodes - Collapsed nodes */ Tree.prototype.getCollapsed = function () { - return this.findNodes('false', 'g', 'state.expanded'); + return this._findNodes('false', 'state.expanded'); }; /** @@ -768,7 +1061,7 @@ @returns {Array} nodes - Checked nodes */ Tree.prototype.getChecked = function () { - return this.findNodes('true', 'g', 'state.checked'); + return this._findNodes('true', 'state.checked'); }; /** @@ -776,7 +1069,7 @@ @returns {Array} nodes - Unchecked nodes */ Tree.prototype.getUnchecked = function () { - return this.findNodes('false', 'g', 'state.checked'); + return this._findNodes('false', 'state.checked'); }; /** @@ -784,7 +1077,7 @@ @returns {Array} nodes - Disabled nodes */ Tree.prototype.getDisabled = function () { - return this.findNodes('true', 'g', 'state.disabled'); + return this._findNodes('true', 'state.disabled'); }; /** @@ -792,47 +1085,208 @@ @returns {Array} nodes - Enabled nodes */ Tree.prototype.getEnabled = function () { - return this.findNodes('false', 'g', 'state.disabled'); + return this._findNodes('false', 'state.disabled'); }; /** - Set a node state to selected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Add nodes to the tree. + @param {Array} nodes - An array of nodes to add + @param {optional Object} parentNode - The node to which nodes will be added as children + @param {optional number} index - Zero based insert index @param {optional Object} options */ - Tree.prototype.selectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, true, options); + Tree.prototype.addNode = function (nodes, parentNode, index, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (parentNode instanceof Array) { + parentNode = parentNode[0]; + } + + options = $.extend({}, _default.options, options); + + // identify target nodes; either the tree's root or a parent's child nodes + var targetNodes; + if (parentNode && parentNode.nodes) { + targetNodes = parentNode.nodes; + } else if (parentNode) { + targetNodes = parentNode.nodes = []; + } else { + targetNodes = this._tree; + } + + // inserting nodes at specified positions + $.each(nodes, $.proxy(function (i, node) { + var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); + targetNodes.splice(insertIndex, 0, node); }, this)); - this.render(); + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done($.proxy(function () { + if (parentNode && !parentNode.state.expanded) { + this._setExpanded(parentNode, true, options); + } + this._render(); + }, this)); + } + + /** + Add nodes to the tree after given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added after + @param {optional Object} options + */ + Tree.prototype.addNodeAfter = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); + } + + /** + Add nodes to the tree before given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added before + @param {optional Object} options + */ + Tree.prototype.addNodeBefore = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], node.index, options); + } + + /** + Removes given nodes from the tree. + @param {Array} nodes - An array of nodes to remove + @param {optional Object} options + */ + Tree.prototype.removeNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + var targetNodes, parentNode; + $.each(nodes, $.proxy(function (index, node) { + + // remove nodes from tree + parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1); + + // remove node from DOM + this._removeNodeEl(node); + }, this)); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); + }; + + /** + Updates / replaces a given tree node + @param {Object} node - A single node to be replaced + @param {Object} newNode - THe replacement node + @param {optional Object} options + */ + Tree.prototype.updateNode = function (node, newNode, options) { + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + // insert new node + var targetNodes; + var parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1, newNode); + + // remove old node from DOM + this._removeNodeEl(node); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); }; + /** - Set a node state to unselected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Selects given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, false, options); + Tree.prototype.selectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, true, options); }, this)); + }; - this.render(); + /** + Unselects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.unselectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, false, options); + }, this)); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleSelectedState(node, options); - }, this)); + Tree.prototype.toggleNodeSelected = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleSelected(node, options); + }, this)); }; @@ -841,25 +1295,22 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); - - this.render(); + options = $.extend({}, _default.options, options); + options.levels = options.levels || 999; + this.collapseNode(this._tree, options); }; /** Collapse a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); + Tree.prototype.collapseNode = function (nodes, options) { + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, false, options); + }, this)); }; /** @@ -868,75 +1319,82 @@ */ Tree.prototype.expandAll = function (options) { options = $.extend({}, _default.options, options); - - if (options && options.levels) { - this.expandLevels(this.tree, options.levels, options); - } - else { - var identifiers = this.findNodes('false', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - }, this)); - } - - this.render(); + options.levels = options.levels || 999; + this.expandNode(this._tree, options); }; /** - Expand a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Expand given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.expandNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, options); + Tree.prototype.expandNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, true, options); + if (node.nodes) { + this._expandLevels(node.nodes, options.levels-1, options); } }, this)); - - this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, options) { + Tree.prototype._expandLevels = function (nodes, level, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false, options); + this._setExpanded(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, options); + this._expandLevels(node.nodes, level-1, options); } }, this)); }; /** - Reveals a given tree node, expanding the tree from node to root. - @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + Reveals given tree nodes, expanding the tree from node to root. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.revealNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - var parentNode = this.getParent(node); - while (parentNode) { - this.setExpandedState(parentNode, true, options); - parentNode = this.getParent(parentNode); + Tree.prototype.revealNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node; + var tmpNode; + while (tmpNode = this.getParents([parentNode])[0]) { + parentNode = tmpNode; + this._setExpanded(parentNode, true, options); }; }, this)); - - this.render(); }; /** - Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleExpandedState(node, options); + Tree.prototype.toggleNodeExpanded = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleExpanded(node, options); }, this)); - - this.render(); }; @@ -945,25 +1403,29 @@ @param {optional Object} options */ Tree.prototype.checkAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** - Check a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Checks given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.checkNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + Tree.prototype.checkNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** @@ -971,38 +1433,46 @@ @param {optional Object} options */ Tree.prototype.uncheckAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Uncheck a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Uncheck given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.uncheckNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + Tree.prototype.uncheckNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Toggles a nodes checked state; checking if unchecked, unchecking if checked. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's checked state; checking if unchecked, unchecking if checked. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeChecked = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleCheckedState(node, options); - }, this)); + Tree.prototype.toggleNodeChecked = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._toggleChecked(node, options); + }, this)); }; @@ -1011,25 +1481,29 @@ @param {optional Object} options */ Tree.prototype.disableAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** - Disable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Disable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.disableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + Tree.prototype.disableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** @@ -1037,65 +1511,48 @@ @param {optional Object} options */ Tree.prototype.enableAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); + }, this)); }; /** - Enable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Enable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.enableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + Tree.prototype.enableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); - }; + options = $.extend({}, _default.options, options); - /** - Toggles a nodes disabled state; disabling is enabled, enabling if disabled. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers - @param {optional Object} options - */ - Tree.prototype.toggleNodeDisabled = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, !node.state.disabled, options); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); }, this)); - - this.render(); }; - /** - Common code for processing multiple identifiers + Toggles a node's disabled state; disabling is enabled, enabling if disabled. + @param {Array} nodes - An array of nodes + @param {optional Object} options */ - Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + Tree.prototype.toggleNodeDisabled = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } options = $.extend({}, _default.options, options); - if (!(identifiers instanceof Array)) { - identifiers = [identifiers]; - } - - $.each(identifiers, $.proxy(function (index, identifier) { - callback(this.identifyNode(identifier), options); - }, this)); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, !node.state.disabled, options); + }, this)); }; - /* - Identifies a node from either a node id or object - */ - Tree.prototype.identifyNode = function (identifier) { - return ((typeof identifier) === 'number') ? - this.nodes[identifier] : - identifier; - }; /** Searches the tree for nodes (text) that match given criteria @@ -1106,9 +1563,9 @@ Tree.prototype.search = function (pattern, options) { options = $.extend({}, _default.searchOptions, options); - this.clearSearch({ render: false }); - + var previous = this._getSearchResults(); var results = []; + if (pattern && pattern.length > 0) { if (options.exactMatch) { @@ -1120,26 +1577,25 @@ modifier += 'i'; } - results = this.findNodes(pattern, modifier); - - // Add searchResult property to all matching nodes - // This will be used to apply custom styles - // and when identifying result to be cleared - $.each(results, function (index, node) { - node.searchResult = true; - }) + results = this._findNodes(pattern, 'text', modifier); } - // If revealResults, then render is triggered from revealNode - // otherwise we just call render. - if (options.revealResults) { + // Clear previous results no longer matched + $.each(this._diffArray(results, previous), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); + + // Set new results + $.each(this._diffArray(previous, results), $.proxy(function (index, node) { + this._setSearchResult(node, true, options); + }, this)); + + // Reveal hidden nodes + if (results && options.revealResults) { this.revealNode(results); } - else { - this.render(); - } - this.$element.trigger('searchComplete', $.extend(true, {}, results)); + this._triggerEvent('searchComplete', results, options); return results; }; @@ -1148,39 +1604,45 @@ Clears previous search results */ Tree.prototype.clearSearch = function (options) { - options = $.extend({}, { render: true }, options); - var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { - node.searchResult = false; - }); + var results = $.each(this._getSearchResults(), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); - if (options.render) { - this.render(); - } - - this.$element.trigger('searchCleared', $.extend(true, {}, results)); + this._triggerEvent('searchCleared', results, options); + }; + + Tree.prototype._getSearchResults = function () { + return this._findNodes('true', 'searchResult'); + }; + + Tree.prototype._diffArray = function (a, b) { + var diff = []; + $.grep(b, function (n) { + if ($.inArray(n, a) === -1) { + diff.push(n); + } + }); + return diff; }; /** Find nodes that match a given criteria @param {String} pattern - A given string to match against - @param {optional String} modifier - Valid RegEx modifiers @param {optional String} attribute - Attribute to compare pattern against + @param {optional String} modifier - Valid RegEx modifiers @return {Array} nodes - Nodes that match your criteria */ - Tree.prototype.findNodes = function (pattern, modifier, attribute) { - - modifier = modifier || 'g'; + Tree.prototype._findNodes = function (pattern, attribute, modifier) { attribute = attribute || 'text'; - - var _this = this; - return $.grep(this.nodes, function (node) { - var val = _this.getNodeValue(node, attribute); + modifier = modifier || 'g'; + return $.grep(this._orderedNodes, $.proxy(function (node) { + var val = this._getNodeValue(node, attribute); if (typeof val === 'string') { return val.match(new RegExp(pattern, modifier)); } - }); + }, this)); }; /** @@ -1190,12 +1652,12 @@ @param {String} attr - Identifies an object property using dot notation @return {String} value - Matching attributes string representation */ - Tree.prototype.getNodeValue = function (obj, attr) { + Tree.prototype._getNodeValue = function (obj, attr) { var index = attr.indexOf('.'); if (index > 0) { var _obj = obj[attr.substring(0, index)]; var _attr = attr.substring(index + 1, attr.length); - return this.getNodeValue(_obj, _attr); + return this._getNodeValue(_obj, _attr); } else { if (obj.hasOwnProperty(attr)) { diff --git a/src/css/bootstrap-treeview.css b/src/css/bootstrap-treeview.css index 23c6cf066..bccab55ab 100644 --- a/src/css/bootstrap-treeview.css +++ b/src/css/bootstrap-treeview.css @@ -1,9 +1,9 @@ /* ========================================================= - * bootstrap-treeview.css v1.2.0 + * bootstrap-treeview.css v2.0.0 * ========================================================= - * Copyright 2013 Jonathan Miles + * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -34,4 +34,8 @@ .treeview .node-disabled { color: silver; cursor: not-allowed; -} \ No newline at end of file +} + +.treeview .node-hidden { + display: none; +} diff --git a/src/js/bootstrap-treeview.js b/src/js/bootstrap-treeview.js index 7a82a2eeb..820a1f941 100644 --- a/src/js/bootstrap-treeview.js +++ b/src/js/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.2.0 + * bootstrap-treeview.js v2.0.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -41,16 +41,15 @@ checkedIcon: 'glyphicon glyphicon-check', uncheckedIcon: 'glyphicon glyphicon-unchecked', - color: undefined, // '#000000', - backColor: undefined, // '#FFFFFF', - borderColor: undefined, // '#dddddd', + color: undefined, + backColor: undefined, + borderColor: undefined, onhoverColor: '#F5F5F5', selectedColor: '#FFFFFF', selectedBackColor: '#428bca', searchResultColor: '#D9534F', - searchResultBackColor: undefined, //'#FFFFFF', + searchResultBackColor: undefined, - enableLinks: false, highlightSelected: true, highlightSearchResults: true, showBorder: true, @@ -58,8 +57,16 @@ showCheckbox: false, showTags: false, multiSelect: false, + preventUnselect: false, // Event handlers + onLoading: undefined, + onLoadingFailed: undefined, + onInitialized: undefined, + onNodeRendered: undefined, + onRendered: undefined, + onDestroyed: undefined, + onNodeChecked: undefined, onNodeCollapsed: undefined, onNodeDisabled: undefined, @@ -68,6 +75,7 @@ onNodeSelected: undefined, onNodeUnchecked: undefined, onNodeUnselected: undefined, + onSearchComplete: undefined, onSearchCleared: undefined }; @@ -83,26 +91,32 @@ revealResults: true }; - var Tree = function (element, options) { + _default.dataUrl = { + method: 'GET', + dataType: 'json', + cache: false + }; + var Tree = function (element, options) { this.$element = $(element); - this.elementId = element.id; - this.styleId = this.elementId + '-style'; + this._elementId = element.id; + this._styleId = this._elementId + '-style'; - this.init(options); + this._init(options); return { // Options (public access) - options: this.options, + options: this._options, // Initialize / destroy methods - init: $.proxy(this.init, this), - remove: $.proxy(this.remove, this), + init: $.proxy(this._init, this), + remove: $.proxy(this._remove, this), - // Get methods - getNode: $.proxy(this.getNode, this), - getParent: $.proxy(this.getParent, this), + // Query methods + findNodes: $.proxy(this.findNodes, this), + getNodes: $.proxy(this.getNodes, this), // todo document + test + getParents: $.proxy(this.getParents, this), getSiblings: $.proxy(this.getSiblings, this), getSelected: $.proxy(this.getSelected, this), getUnselected: $.proxy(this.getUnselected, this), @@ -113,6 +127,13 @@ getDisabled: $.proxy(this.getDisabled, this), getEnabled: $.proxy(this.getEnabled, this), + // Tree manipulation methods + addNode: $.proxy(this.addNode, this), + addNodeAfter: $.proxy(this.addNodeAfter, this), + addNodeBefore: $.proxy(this.addNodeBefore, this), + removeNode: $.proxy(this.removeNode, this), + updateNode: $.proxy(this.updateNode, this), + // Select methods selectNode: $.proxy(this.selectNode, this), unselectNode: $.proxy(this.unselectNode, this), @@ -126,7 +147,7 @@ toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), revealNode: $.proxy(this.revealNode, this), - // Expand / collapse methods + // Check / uncheck methods checkAll: $.proxy(this.checkAll, this), checkNode: $.proxy(this.checkNode, this), uncheckAll: $.proxy(this.uncheckAll, this), @@ -146,48 +167,87 @@ }; }; - Tree.prototype.init = function (options) { - - this.tree = []; - this.nodes = []; + Tree.prototype._init = function (options) { + this._tree = []; + this._initialized = false; + + this._options = $.extend({}, _default.settings, options); + + this._destroy(); + this._subscribeEvents(); + + this._triggerEvent('loading', null, _default.options); + this._load(options) + .then($.proxy(function (data) { + // load done + return this._tree = $.extend(true, [], data); + }, this), $.proxy(function (error) { + // load fail + this._triggerEvent('loadingFailed', error, _default.options); + }, this)) + .then($.proxy(function (treeData) { + // initialize data + return this._setInitialStates({ nodes: treeData }, 0); + }, this)) + .then($.proxy(function () { + // render to DOM + this._render(); + }, this)); + }; + Tree.prototype._load = function (options) { + var done = new $.Deferred(); if (options.data) { - if (typeof options.data === 'string') { - options.data = $.parseJSON(options.data); - } - this.tree = $.extend(true, [], options.data); - delete options.data; + this._loadLocalData(options, done); + } else if (options.dataUrl) { + this._loadRemoteData(options, done); } - this.options = $.extend({}, _default.settings, options); + return done.promise(); + }; - this.destroy(); - this.subscribeEvents(); - this.setInitialStates({ nodes: this.tree }, 0); - this.render(); + Tree.prototype._loadRemoteData = function (options, done) { + $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl)) + .done(function (data) { + done.resolve(data); + }) + .fail(function (xhr, status, error) { + done.reject(error); + }); }; - Tree.prototype.remove = function () { - this.destroy(); - $.removeData(this, pluginName); - $('#' + this.styleId).remove(); + Tree.prototype._loadLocalData = function (options, done) { + done.resolve((typeof options.data === 'string') ? + $.parseJSON(options.data) : + $.extend(true, [], options.data)); }; - Tree.prototype.destroy = function () { + Tree.prototype._remove = function () { + this._destroy(); + $.removeData(this, pluginName); + $('#' + this._styleId).remove(); + }; - if (!this.initialized) return; + Tree.prototype._destroy = function () { + if (!this._initialized) return; + this._initialized = false; - this.$wrapper.remove(); - this.$wrapper = null; + this._triggerEvent('destroyed', null, _default.options); // Switch off events - this.unsubscribeEvents(); + this._unsubscribeEvents(); - // Reset this.initialized flag - this.initialized = false; + // Tear down + this.$wrapper.remove(); + this.$wrapper = null; }; - Tree.prototype.unsubscribeEvents = function () { - + Tree.prototype._unsubscribeEvents = function () { + this.$element.off('loading'); + this.$element.off('loadingFailed'); + this.$element.off('initialized'); + this.$element.off('nodeRendered'); + this.$element.off('rendered'); + this.$element.off('destroyed'); this.$element.off('click'); this.$element.off('nodeChecked'); this.$element.off('nodeCollapsed'); @@ -201,70 +261,118 @@ this.$element.off('searchCleared'); }; - Tree.prototype.subscribeEvents = function () { + Tree.prototype._subscribeEvents = function () { + this._unsubscribeEvents(); + + if (typeof (this._options.onLoading) === 'function') { + this.$element.on('loading', this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.$element.on('loadingFailed', this._options.onLoadingFailed); + } - this.unsubscribeEvents(); + if (typeof (this._options.onInitialized) === 'function') { + this.$element.on('initialized', this._options.onInitialized); + } - this.$element.on('click', $.proxy(this.clickHandler, this)); + if (typeof (this._options.onNodeRendered) === 'function') { + this.$element.on('nodeRendered', this._options.onNodeRendered); + } - if (typeof (this.options.onNodeChecked) === 'function') { - this.$element.on('nodeChecked', this.options.onNodeChecked); + if (typeof (this._options.onRendered) === 'function') { + this.$element.on('rendered', this._options.onRendered); } - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); + if (typeof (this._options.onDestroyed) === 'function') { + this.$element.on('destroyed', this._options.onDestroyed); } - if (typeof (this.options.onNodeDisabled) === 'function') { - this.$element.on('nodeDisabled', this.options.onNodeDisabled); + this.$element.on('click', $.proxy(this._clickHandler, this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.$element.on('nodeChecked', this._options.onNodeChecked); } - if (typeof (this.options.onNodeEnabled) === 'function') { - this.$element.on('nodeEnabled', this.options.onNodeEnabled); + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.$element.on('nodeCollapsed', this._options.onNodeCollapsed); } - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.on('nodeExpanded', this.options.onNodeExpanded); + if (typeof (this._options.onNodeDisabled) === 'function') { + this.$element.on('nodeDisabled', this._options.onNodeDisabled); } - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.on('nodeSelected', this.options.onNodeSelected); + if (typeof (this._options.onNodeEnabled) === 'function') { + this.$element.on('nodeEnabled', this._options.onNodeEnabled); } - if (typeof (this.options.onNodeUnchecked) === 'function') { - this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); + if (typeof (this._options.onNodeExpanded) === 'function') { + this.$element.on('nodeExpanded', this._options.onNodeExpanded); } - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.on('nodeUnselected', this.options.onNodeUnselected); + if (typeof (this._options.onNodeSelected) === 'function') { + this.$element.on('nodeSelected', this._options.onNodeSelected); } - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.on('searchComplete', this.options.onSearchComplete); + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.$element.on('nodeUnchecked', this._options.onNodeUnchecked); } - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.on('searchCleared', this.options.onSearchCleared); + if (typeof (this._options.onNodeUnselected) === 'function') { + this.$element.on('nodeUnselected', this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.$element.on('searchComplete', this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.$element.on('searchCleared', this._options.onSearchCleared); } }; + Tree.prototype._triggerEvent = function (event, data, options) { + if (options && !options.silent) { + this.$element.trigger(event, $.extend(true, {}, data)); + } + } + /* Recurse the tree structure and ensure all nodes have valid initial states. User defined states will be preserved. For performance we also take this opportunity to - index nodes in a flattened structure + index nodes in a flattened ordered structure */ - Tree.prototype.setInitialStates = function (node, level) { + Tree.prototype._setInitialStates = function (node, level) { + this._nodes = {}; + return $.when.apply(this, this._setInitialState(node, level)) + .done($.proxy(function () { + this._orderedNodes = this._sortNodes(); + this._triggerEvent('initialized', this._orderedNodes, _default.options); + return; + }, this)); + }; + Tree.prototype._setInitialState = function (node, level, done) { if (!node.nodes) return; level += 1; + done = done || []; var parent = node; - var _this = this; - $.each(node.nodes, function checkStates(index, node) { + $.each(node.nodes, $.proxy(function (index, node) { + var deferred = new $.Deferred(); + done.push(deferred.promise()); + + // level : hierarchical tree level, starts at 1 + node.level = level; - // nodeId : unique, incremental identifier - node.nodeId = _this.nodes.length; + // index : relative to siblings + node.index = index; + + // nodeId : unique, hierarchical identifier + node.nodeId = (parent && parent.nodeId) ? + parent.nodeId + '.' + node.index : + (level - 1) + '.' + node.index; // parentId : transversing up the tree node.parentId = parent.nodeId; @@ -290,7 +398,7 @@ // set expanded state; if not provided based on levels if (!node.state.hasOwnProperty('expanded')) { if (!node.state.disabled && - (level < _this.options.levels) && + (level < this._options.levels) && (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } @@ -304,431 +412,616 @@ node.state.selected = false; } - // index nodes in a flattened structure for use later - _this.nodes.push(node); + // set visible state; based parent state plus levels + if ((parent && parent.state && parent.state.expanded) || + (level <= this._options.levels)) { + node.state.visible = true; + } + else { + node.state.visible = false; + } - // recurse child nodes and transverse the tree + // recurse child nodes and transverse the tree, depth-first if (node.nodes) { - _this.setInitialStates(node, level); + if (node.nodes.length > 0) { + this._setInitialState(node, level, done); + } + else { + delete node.nodes; + } } - }); + + // add / update indexed collection + this._nodes[node.nodeId] = node; + + // mark task as complete + deferred.resolve(); + }, this)); + + return done; }; - Tree.prototype.clickHandler = function (event) { + Tree.prototype._sortNodes = function () { + return $.map(Object.keys(this._nodes).sort(), $.proxy(function (value, index) { + return this._nodes[value]; + }, this)); + }; - if (!this.options.enableLinks) event.preventDefault(); + Tree.prototype._clickHandler = function (event) { var target = $(event.target); - var node = this.findNode(target); + var node = this.targetNode(target); if (!node || node.state.disabled) return; - + var classList = target.attr('class') ? target.attr('class').split(' ') : []; if ((classList.indexOf('expand-icon') !== -1)) { - - this.toggleExpandedState(node, _default.options); - this.render(); + this._toggleExpanded(node, $.extend({}, _default.options)); } else if ((classList.indexOf('check-icon') !== -1)) { - - this.toggleCheckedState(node, _default.options); - this.render(); + this._toggleChecked(node, $.extend({}, _default.options)); } else { - if (node.selectable) { - this.toggleSelectedState(node, _default.options); + this._toggleSelected(node, $.extend({}, _default.options)); } else { - this.toggleExpandedState(node, _default.options); + this._toggleExpanded(node, $.extend({}, _default.options)); } - - this.render(); } }; // Looks up the DOM for the closest parent list item to retrieve the // data attribute nodeid, which is used to lookup the node in the flattened structure. - Tree.prototype.findNode = function (target) { - - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); - var node = this.nodes[nodeId]; - + Tree.prototype.targetNode = function (target) { + var nodeId = target.closest('li.list-group-item').attr('data-nodeId'); + var node = this._nodes[nodeId]; if (!node) { console.log('Error: node does not exist'); } return node; }; - Tree.prototype.toggleExpandedState = function (node, options) { + Tree.prototype._toggleExpanded = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, options); + this._setExpanded(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, options) { + Tree.prototype._setExpanded = function (node, state, options) { - if (state === node.state.expanded) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.expanded) return; if (state && node.nodes) { - // Expand a node + // Set node state node.state.expanded = true; - if (!options.silent) { - this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .addClass(this._options.collapseIcon); + } + + // Expand children + if (node.nodes && options) { + $.each(node.nodes, $.proxy(function (index, node) { + this._setVisible(node, true, options); + }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeExpanded', node, options); } else if (!state) { - // Collapse a node + // Set node state node.state.expanded = false; - if (!options.silent) { - this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.collapseIcon) + .addClass(this._options.expandIcon); } - // Collapse child nodes - if (node.nodes && !options.ignoreChildren) { + // Collapse children + if (node.nodes && options) { $.each(node.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, options); + this._setVisible(node, false, options); + this._setExpanded(node, false, options); }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeCollapsed', node, options); } }; - Tree.prototype.toggleSelectedState = function (node, options) { + Tree.prototype._setVisible = function (node, state, options) { + + if (options && state === node.state.visible) return; + + if (state) { + + // Set node state + node.state.visible = true; + + // Set element + if (node.$el) { + node.$el.removeClass('node-hidden'); + } + } + else { + + // Set node state to unchecked + node.state.visible = false; + + // Set element + if (node.$el) { + node.$el.addClass('node-hidden'); + } + } + }; + + Tree.prototype._toggleSelected = function (node, options) { if (!node) return; - this.setSelectedState(node, !node.state.selected, options); + this._setSelected(node, !node.state.selected, options); + return this; }; - Tree.prototype.setSelectedState = function (node, state, options) { + Tree.prototype._setSelected = function (node, state, options) { - if (state === node.state.selected) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && (state === node.state.selected)) return; if (state) { // If multiSelect false, unselect previously selected - if (!this.options.multiSelect) { - $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, options); + if (!this._options.multiSelect) { + $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) { + this._setSelected(node, false, $.extend(options, {unselecting: true})); }, this)); } - // Continue selecting node + // Set node state node.state.selected = true; - if (!options.silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.addClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.icon || this._options.nodeIcon) + .addClass(node.selectedIcon || this._options.selectedIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeSelected', node, options); } else { - // Unselect node + // If preventUnselect true + only one remaining selection, disable unselect + if (this._options.preventUnselect && + (options && !options.unselecting) && + (this._findNodes('true', 'state.selected').length === 1)) { + return this; + } + + // Set node state node.state.selected = false; - if (!options.silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.selectedIcon || this._options.selectedIcon) + .addClass(node.icon || this._options.nodeIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeUnselected', node, options); } + + return this; }; - Tree.prototype.toggleCheckedState = function (node, options) { + Tree.prototype._toggleChecked = function (node, options) { if (!node) return; - this.setCheckedState(node, !node.state.checked, options); + this._setChecked(node, !node.state.checked, options); }; - Tree.prototype.setCheckedState = function (node, state, options) { + Tree.prototype._setChecked = function (node, state, options) { - if (state === node.state.checked) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.checked) return; if (state) { - // Check node + // Set node state node.state.checked = true; - if (!options.silent) { - this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .addClass(this._options.checkedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); } else { - // Uncheck node + // Set node state to unchecked node.state.checked = false; - if (!options.silent) { - this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.checkedIcon) + .addClass(this._options.uncheckedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeUnchecked', node, options); } }; - Tree.prototype.setDisabledState = function (node, state, options) { + Tree.prototype._setDisabled = function (node, state, options) { - if (state === node.state.disabled) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.disabled) return; if (state) { - // Disable node + // Set node state to disabled node.state.disabled = true; // Disable all other states - this.setExpandedState(node, false, options); - this.setSelectedState(node, false, options); - this.setCheckedState(node, false, options); + this._setSelected(node, false, options); + this._setChecked(node, false, options); + this._setExpanded(node, false, options); - if (!options.silent) { - this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeDisabled', node, options); } else { - // Enabled node + // Set node state to enabled node.state.disabled = false; - if (!options.silent) { - this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeEnabled', node, options); } }; - Tree.prototype.render = function () { + Tree.prototype._setSearchResult = function (node, state, options) { + if (options && state === node.searchResult) return; - if (!this.initialized) { - - // Setup first time only components - this.$element.addClass(pluginName); - this.$wrapper = $(this.template.list); + if (state) { - this.injectStyle(); + node.searchResult = true; - this.initialized = true; + if (node.$el) { + node.$el.addClass('node-result'); + } } + else { - this.$element.empty().append(this.$wrapper.empty()); + node.searchResult = false; - // Build tree - this.buildTree(this.tree, 0); + if (node.$el) { + node.$el.removeClass('node-result'); + } + } }; - // Starting from the root node, and recursing down the - // structure we build the tree one node at a time - Tree.prototype.buildTree = function (nodes, level) { + Tree.prototype._render = function () { + if (!this._initialized) { - if (!nodes) return; - level += 1; + // Setup first time only components + this.$wrapper = $(this._template.tree); + this.$element.empty() + .addClass(pluginName) + .append(this.$wrapper); - var _this = this; - $.each(nodes, function addNodes(id, node) { - - var treeItem = $(_this.template.item) - .addClass('node-' + _this.elementId) - .addClass(node.state.checked ? 'node-checked' : '') - .addClass(node.state.disabled ? 'node-disabled': '') - .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') - .attr('data-nodeid', node.nodeId) - .attr('style', _this.buildStyleOverride(node)); - - // Add indent/spacer to mimic tree structure - for (var i = 0; i < (level - 1); i++) { - treeItem.append(_this.template.indent); - } + this._injectStyle(); - // Add expand, collapse or empty spacer icons - var classList = []; - if (node.nodes) { - classList.push('expand-icon'); - if (node.state.expanded) { - classList.push(_this.options.collapseIcon); - } - else { - classList.push(_this.options.expandIcon); - } - } - else { - classList.push(_this.options.emptyIcon); - } + this._initialized = true; + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); + var previousNode; + $.each(this._orderedNodes, $.proxy(function (id, node) { + this._renderNode(node, previousNode); + previousNode = node; + }, this)); + this._triggerEvent('rendered', this._orderedNodes, _default.options); + }; - // Add node icon - if (_this.options.showIcon) { - - var classList = ['node-icon']; + Tree.prototype._renderNode = function (node, previousNode) { + if (!node) return; - classList.push(node.icon || _this.options.nodeIcon); - if (node.state.selected) { - classList.pop(); - classList.push(node.selectedIcon || _this.options.selectedIcon || - node.icon || _this.options.nodeIcon); - } + if (!node.$el) { + node.$el = this._newNodeEl(node, previousNode) + .addClass('node-' + this._elementId); + } + else { + node.$el.empty(); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Set / update nodeid; it can change as a result of addNode etc. + node.$el.attr('data-nodeId', node.nodeId); - // Add check / unchecked icon - if (_this.options.showCheckbox) { + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (node.level - 1); i++) { + node.$el.append(this._template.indent); + } - var classList = ['check-icon']; - if (node.state.checked) { - classList.push(_this.options.checkedIcon); - } - else { - classList.push(_this.options.uncheckedIcon); - } + // Add expand / collapse or empty spacer icons + node.$el + .append($(this._template.icon) + .addClass(node.nodes ? 'expand-icon' : this._options.emptyIcon) + ); + + // Add node icon + if (this._options.showIcon) { + node.$el + .append($(this._template.icon) + .addClass('node-icon') + .addClass(node.icon || this._options.nodeIcon) + ); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Add checkable icon + if (this._options.showCheckbox) { + node.$el + .append($(this._template.icon) + .addClass('check-icon') + ); + } - // Add text - if (_this.options.enableLinks) { - // Add hyperlink - treeItem - .append($(_this.template.link) - .attr('href', node.href) - .append(node.text) - ); - } - else { - // otherwise just text - treeItem - .append(node.text); - } + // Add text + node.$el.append(node.text); - // Add tags as badges - if (_this.options.showTags && node.tags) { - $.each(node.tags, function addTag(id, tag) { - treeItem - .append($(_this.template.badge) - .append(tag) - ); - }); - } + // Add tags as badges + if (this._options.showTags && node.tags) { + $.each(node.tags, $.proxy(function addTag(id, tag) { + node.$el + .append($(this._template.badge) + .append(tag) + ); + }, this)); + } - // Add item to the tree - _this.$wrapper.append(treeItem); + // Set various node states + this._setSelected(node, node.state.selected); + this._setChecked(node, node.state.checked); + this._setSearchResult(node, node.searchResult); + this._setExpanded(node, node.state.expanded); + this._setDisabled(node, node.state.disabled); + this._setVisible(node, node.state.visible); + + // Trigger nodeRendered event + this._triggerEvent('nodeRendered', node, _default.options); + }; + + // Creates a new node element from template and + // ensures the template is inserted at the correct position + Tree.prototype._newNodeEl = function (node, previousNode) { + var $el = $(this._template.node); + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + this.$wrapper.children() + .eq(previousNode.$el.index()) + .after($el); + } else { + // we use prepend instead of append, + // to cater for root inserts i.e. nodeId 0.0 + this.$wrapper.prepend($el); + } - // Recursively add child ndoes - if (node.nodes && node.state.expanded && !node.state.disabled) { - return _this.buildTree(node.nodes, level); - } - }); + return $el; }; - // Define any node level style override for - // 1. selectedNode - // 2. node|data assigned color overrides - Tree.prototype.buildStyleOverride = function (node) { - - if (node.state.disabled) return ''; - - var color = node.color; - var backColor = node.backColor; + // Recursively remove node elements from DOM + Tree.prototype._removeNodeEl = function (node) { + if (!node) return; - if (this.options.highlightSelected && node.state.selected) { - if (this.options.selectedColor) { - color = this.options.selectedColor; - } - if (this.options.selectedBackColor) { - backColor = this.options.selectedBackColor; - } + if (node.nodes) { + $.each(node.nodes, $.proxy(function (index, node) { + this._removeNodeEl(node); + }, this)); } + node.$el.remove(); + }; - if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { - if (this.options.searchResultColor) { - color = this.options.searchResultColor; - } - if (this.options.searchResultBackColor) { - backColor = this.options.searchResultBackColor; - } - } + // Expand node, rendering it's immediate children + Tree.prototype._expandNode = function (node) { + if (!node.nodes) return; - return 'color:' + color + - ';background-color:' + backColor + ';'; + $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) { + childNode.level = node.level + 1; + this._renderNode(childNode, node.$el); + }, this)); }; // Add inline style into head - Tree.prototype.injectStyle = function () { - - if (this.options.injectStyle && !document.getElementById(this.styleId)) { - $('').appendTo('head'); + Tree.prototype._injectStyle = function () { + if (this._options.injectStyle && !document.getElementById(this._styleId)) { + $('').appendTo('head'); } }; // Construct trees style based on user options - Tree.prototype.buildStyle = function () { - - var style = '.node-' + this.elementId + '{'; + Tree.prototype._buildStyle = function () { + var style = '.node-' + this._elementId + '{'; - if (this.options.color) { - style += 'color:' + this.options.color + ';'; + // Basic bootstrap style overrides + if (this._options.color) { + style += 'color:' + this._options.color + ';'; } - if (this.options.backColor) { - style += 'background-color:' + this.options.backColor + ';'; + if (this._options.backColor) { + style += 'background-color:' + this._options.backColor + ';'; } - if (!this.options.showBorder) { + if (!this._options.showBorder) { style += 'border:none;'; } - else if (this.options.borderColor) { - style += 'border:1px solid ' + this.options.borderColor + ';'; + else if (this._options.borderColor) { + style += 'border:1px solid ' + this._options.borderColor + ';'; } style += '}'; - if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + if (this._options.onhoverColor) { + style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this._options.onhoverColor + ';' + '}'; } - return this.css + style; + // Style search results + if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { + + var innerStyle = '' + if (this._options.searchResultColor) { + innerStyle += 'color:' + this._options.searchResultColor + ';'; + } + if (this._options.searchResultBackColor) { + innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; + } + + // Style selected nodes + if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { + + var innerStyle = '' + if (this._options.selectedColor) { + innerStyle += 'color:' + this._options.selectedColor + ';'; + } + if (this._options.selectedBackColor) { + innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; + } + + // Node level style overrides + $.each(this._orderedNodes, $.proxy(function (index, node) { + if (node.color || node.backColor) { + var innerStyle = ''; + if (node.color) { + innerStyle += 'color:' + node.color + ';'; + } + if (node.backColor) { + innerStyle += 'background-color:' + node.backColor + ';'; + } + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; + } + }, this)); + + return this._css + style; }; - Tree.prototype.template = { - list: '
            ', - item: '
          • ', + Tree.prototype._template = { + tree: '
              ', + node: '
            • ', indent: '', icon: '', - link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + Tree.prototype._css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + Returns an array of matching node objects. + @param {String} pattern - A pattern to match against a given field + @return {String} field - Field to query pattern against + */ + Tree.prototype.findNodes = function (pattern, field) { + return this._findNodes(pattern, field); + }; /** - Returns a single node object that matches the given node id. - @param {Number} nodeId - A node's unique identifier - @return {Object} node - Matching node + Returns an ordered aarray of node objects. + @return {Array} nodes - An array of all nodes */ - Tree.prototype.getNode = function (nodeId) { - return this.nodes[nodeId]; + Tree.prototype.getNodes = function () { + return this._orderedNodes; }; /** - Returns the parent node of a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Object} node - The parent node + Returns parent nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of parent nodes */ - Tree.prototype.getParent = function (identifier) { - var node = this.identifyNode(identifier); - return this.nodes[node.parentId]; + Tree.prototype.getParents = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var parentNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node.parentId ? this._nodes[node.parentId] : false; + if (parentNode) { + parentNodes.push(parentNode); + } + }, this)); + return parentNodes; }; /** - Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Array} nodes - Sibling nodes + Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of sibling nodes */ - Tree.prototype.getSiblings = function (identifier) { - var node = this.identifyNode(identifier); - var parent = this.getParent(node); - var nodes = parent ? parent.nodes : this.tree; - return nodes.filter(function (obj) { + Tree.prototype.getSiblings = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var siblingNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parent = this.getParents([node]); + var nodes = parent[0] ? parent[0].nodes : this._tree; + siblingNodes = nodes.filter(function (obj) { return obj.nodeId !== node.nodeId; }); + }, this)); + + // flatten possible nested array before returning + return $.map(siblingNodes, function (obj) { + return obj; + }); }; /** @@ -736,7 +1029,7 @@ @returns {Array} nodes - Selected nodes */ Tree.prototype.getSelected = function () { - return this.findNodes('true', 'g', 'state.selected'); + return this._findNodes('true', 'state.selected'); }; /** @@ -744,7 +1037,7 @@ @returns {Array} nodes - Unselected nodes */ Tree.prototype.getUnselected = function () { - return this.findNodes('false', 'g', 'state.selected'); + return this._findNodes('false', 'state.selected'); }; /** @@ -752,7 +1045,7 @@ @returns {Array} nodes - Expanded nodes */ Tree.prototype.getExpanded = function () { - return this.findNodes('true', 'g', 'state.expanded'); + return this._findNodes('true', 'state.expanded'); }; /** @@ -760,7 +1053,7 @@ @returns {Array} nodes - Collapsed nodes */ Tree.prototype.getCollapsed = function () { - return this.findNodes('false', 'g', 'state.expanded'); + return this._findNodes('false', 'state.expanded'); }; /** @@ -768,7 +1061,7 @@ @returns {Array} nodes - Checked nodes */ Tree.prototype.getChecked = function () { - return this.findNodes('true', 'g', 'state.checked'); + return this._findNodes('true', 'state.checked'); }; /** @@ -776,7 +1069,7 @@ @returns {Array} nodes - Unchecked nodes */ Tree.prototype.getUnchecked = function () { - return this.findNodes('false', 'g', 'state.checked'); + return this._findNodes('false', 'state.checked'); }; /** @@ -784,7 +1077,7 @@ @returns {Array} nodes - Disabled nodes */ Tree.prototype.getDisabled = function () { - return this.findNodes('true', 'g', 'state.disabled'); + return this._findNodes('true', 'state.disabled'); }; /** @@ -792,47 +1085,208 @@ @returns {Array} nodes - Enabled nodes */ Tree.prototype.getEnabled = function () { - return this.findNodes('false', 'g', 'state.disabled'); + return this._findNodes('false', 'state.disabled'); }; /** - Set a node state to selected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Add nodes to the tree. + @param {Array} nodes - An array of nodes to add + @param {optional Object} parentNode - The node to which nodes will be added as children + @param {optional number} index - Zero based insert index @param {optional Object} options */ - Tree.prototype.selectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, true, options); + Tree.prototype.addNode = function (nodes, parentNode, index, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (parentNode instanceof Array) { + parentNode = parentNode[0]; + } + + options = $.extend({}, _default.options, options); + + // identify target nodes; either the tree's root or a parent's child nodes + var targetNodes; + if (parentNode && parentNode.nodes) { + targetNodes = parentNode.nodes; + } else if (parentNode) { + targetNodes = parentNode.nodes = []; + } else { + targetNodes = this._tree; + } + + // inserting nodes at specified positions + $.each(nodes, $.proxy(function (i, node) { + var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); + targetNodes.splice(insertIndex, 0, node); }, this)); - this.render(); + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done($.proxy(function () { + if (parentNode && !parentNode.state.expanded) { + this._setExpanded(parentNode, true, options); + } + this._render(); + }, this)); + } + + /** + Add nodes to the tree after given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added after + @param {optional Object} options + */ + Tree.prototype.addNodeAfter = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); + } + + /** + Add nodes to the tree before given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added before + @param {optional Object} options + */ + Tree.prototype.addNodeBefore = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], node.index, options); + } + + /** + Removes given nodes from the tree. + @param {Array} nodes - An array of nodes to remove + @param {optional Object} options + */ + Tree.prototype.removeNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + var targetNodes, parentNode; + $.each(nodes, $.proxy(function (index, node) { + + // remove nodes from tree + parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1); + + // remove node from DOM + this._removeNodeEl(node); + }, this)); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); + }; + + /** + Updates / replaces a given tree node + @param {Object} node - A single node to be replaced + @param {Object} newNode - THe replacement node + @param {optional Object} options + */ + Tree.prototype.updateNode = function (node, newNode, options) { + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + // insert new node + var targetNodes; + var parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1, newNode); + + // remove old node from DOM + this._removeNodeEl(node); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); }; + /** - Set a node state to unselected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Selects given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, false, options); + Tree.prototype.selectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, true, options); }, this)); + }; - this.render(); + /** + Unselects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.unselectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, false, options); + }, this)); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleSelectedState(node, options); - }, this)); + Tree.prototype.toggleNodeSelected = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleSelected(node, options); + }, this)); }; @@ -841,25 +1295,22 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); - - this.render(); + options = $.extend({}, _default.options, options); + options.levels = options.levels || 999; + this.collapseNode(this._tree, options); }; /** Collapse a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); + Tree.prototype.collapseNode = function (nodes, options) { + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, false, options); + }, this)); }; /** @@ -868,75 +1319,82 @@ */ Tree.prototype.expandAll = function (options) { options = $.extend({}, _default.options, options); - - if (options && options.levels) { - this.expandLevels(this.tree, options.levels, options); - } - else { - var identifiers = this.findNodes('false', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - }, this)); - } - - this.render(); + options.levels = options.levels || 999; + this.expandNode(this._tree, options); }; /** - Expand a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Expand given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.expandNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, options); + Tree.prototype.expandNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, true, options); + if (node.nodes) { + this._expandLevels(node.nodes, options.levels-1, options); } }, this)); - - this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, options) { + Tree.prototype._expandLevels = function (nodes, level, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false, options); + this._setExpanded(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, options); + this._expandLevels(node.nodes, level-1, options); } }, this)); }; /** - Reveals a given tree node, expanding the tree from node to root. - @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + Reveals given tree nodes, expanding the tree from node to root. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.revealNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - var parentNode = this.getParent(node); - while (parentNode) { - this.setExpandedState(parentNode, true, options); - parentNode = this.getParent(parentNode); + Tree.prototype.revealNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node; + var tmpNode; + while (tmpNode = this.getParents([parentNode])[0]) { + parentNode = tmpNode; + this._setExpanded(parentNode, true, options); }; }, this)); - - this.render(); }; /** - Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleExpandedState(node, options); + Tree.prototype.toggleNodeExpanded = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleExpanded(node, options); }, this)); - - this.render(); }; @@ -945,25 +1403,29 @@ @param {optional Object} options */ Tree.prototype.checkAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** - Check a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Checks given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.checkNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + Tree.prototype.checkNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** @@ -971,38 +1433,46 @@ @param {optional Object} options */ Tree.prototype.uncheckAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Uncheck a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Uncheck given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.uncheckNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + Tree.prototype.uncheckNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Toggles a nodes checked state; checking if unchecked, unchecking if checked. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's checked state; checking if unchecked, unchecking if checked. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeChecked = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleCheckedState(node, options); - }, this)); + Tree.prototype.toggleNodeChecked = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._toggleChecked(node, options); + }, this)); }; @@ -1011,25 +1481,29 @@ @param {optional Object} options */ Tree.prototype.disableAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** - Disable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Disable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.disableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + Tree.prototype.disableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** @@ -1037,65 +1511,48 @@ @param {optional Object} options */ Tree.prototype.enableAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); + }, this)); }; /** - Enable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Enable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.enableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + Tree.prototype.enableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); - }; + options = $.extend({}, _default.options, options); - /** - Toggles a nodes disabled state; disabling is enabled, enabling if disabled. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers - @param {optional Object} options - */ - Tree.prototype.toggleNodeDisabled = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, !node.state.disabled, options); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); }, this)); - - this.render(); }; - /** - Common code for processing multiple identifiers + Toggles a node's disabled state; disabling is enabled, enabling if disabled. + @param {Array} nodes - An array of nodes + @param {optional Object} options */ - Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + Tree.prototype.toggleNodeDisabled = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } options = $.extend({}, _default.options, options); - if (!(identifiers instanceof Array)) { - identifiers = [identifiers]; - } - - $.each(identifiers, $.proxy(function (index, identifier) { - callback(this.identifyNode(identifier), options); - }, this)); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, !node.state.disabled, options); + }, this)); }; - /* - Identifies a node from either a node id or object - */ - Tree.prototype.identifyNode = function (identifier) { - return ((typeof identifier) === 'number') ? - this.nodes[identifier] : - identifier; - }; /** Searches the tree for nodes (text) that match given criteria @@ -1106,9 +1563,9 @@ Tree.prototype.search = function (pattern, options) { options = $.extend({}, _default.searchOptions, options); - this.clearSearch({ render: false }); - + var previous = this._getSearchResults(); var results = []; + if (pattern && pattern.length > 0) { if (options.exactMatch) { @@ -1120,26 +1577,25 @@ modifier += 'i'; } - results = this.findNodes(pattern, modifier); - - // Add searchResult property to all matching nodes - // This will be used to apply custom styles - // and when identifying result to be cleared - $.each(results, function (index, node) { - node.searchResult = true; - }) + results = this._findNodes(pattern, 'text', modifier); } - // If revealResults, then render is triggered from revealNode - // otherwise we just call render. - if (options.revealResults) { + // Clear previous results no longer matched + $.each(this._diffArray(results, previous), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); + + // Set new results + $.each(this._diffArray(previous, results), $.proxy(function (index, node) { + this._setSearchResult(node, true, options); + }, this)); + + // Reveal hidden nodes + if (results && options.revealResults) { this.revealNode(results); } - else { - this.render(); - } - this.$element.trigger('searchComplete', $.extend(true, {}, results)); + this._triggerEvent('searchComplete', results, options); return results; }; @@ -1148,39 +1604,45 @@ Clears previous search results */ Tree.prototype.clearSearch = function (options) { - options = $.extend({}, { render: true }, options); - var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { - node.searchResult = false; - }); + var results = $.each(this._getSearchResults(), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); - if (options.render) { - this.render(); - } - - this.$element.trigger('searchCleared', $.extend(true, {}, results)); + this._triggerEvent('searchCleared', results, options); + }; + + Tree.prototype._getSearchResults = function () { + return this._findNodes('true', 'searchResult'); + }; + + Tree.prototype._diffArray = function (a, b) { + var diff = []; + $.grep(b, function (n) { + if ($.inArray(n, a) === -1) { + diff.push(n); + } + }); + return diff; }; /** Find nodes that match a given criteria @param {String} pattern - A given string to match against - @param {optional String} modifier - Valid RegEx modifiers @param {optional String} attribute - Attribute to compare pattern against + @param {optional String} modifier - Valid RegEx modifiers @return {Array} nodes - Nodes that match your criteria */ - Tree.prototype.findNodes = function (pattern, modifier, attribute) { - - modifier = modifier || 'g'; + Tree.prototype._findNodes = function (pattern, attribute, modifier) { attribute = attribute || 'text'; - - var _this = this; - return $.grep(this.nodes, function (node) { - var val = _this.getNodeValue(node, attribute); + modifier = modifier || 'g'; + return $.grep(this._orderedNodes, $.proxy(function (node) { + var val = this._getNodeValue(node, attribute); if (typeof val === 'string') { return val.match(new RegExp(pattern, modifier)); } - }); + }, this)); }; /** @@ -1190,12 +1652,12 @@ @param {String} attr - Identifies an object property using dot notation @return {String} value - Matching attributes string representation */ - Tree.prototype.getNodeValue = function (obj, attr) { + Tree.prototype._getNodeValue = function (obj, attr) { var index = attr.indexOf('.'); if (index > 0) { var _obj = obj[attr.substring(0, index)]; var _attr = attr.substring(index + 1, attr.length); - return this.getNodeValue(_obj, _attr); + return this._getNodeValue(_obj, _attr); } else { if (obj.hasOwnProperty(attr)) { diff --git a/tests/data.json b/tests/data.json new file mode 100644 index 000000000..ff16499ae --- /dev/null +++ b/tests/data.json @@ -0,0 +1,42 @@ +[ + { + "text": "Parent 1", + "tags": [4], + "nodes": [ + { + "text": "Child 1", + "tags": [2], + "nodes": [ + { + "text": "Grandchild 1", + "tags": [0] + }, + { + "text": "Grandchild 2", + "tags": [0] + } + ] + }, + { + "text": "Child 2", + "tags": [0] + } + ] + }, + { + "text": "Parent 2", + "tags": [0] + }, + { + "text": "Parent 3", + "tags": [0] + }, + { + "text": "Parent 4", + "tags": [0] + }, + { + "text": "Parent 5", + "tags": [0] + } +] diff --git a/tests/lib/bootstrap-treeview.css b/tests/lib/bootstrap-treeview.css index 23c6cf066..bccab55ab 100644 --- a/tests/lib/bootstrap-treeview.css +++ b/tests/lib/bootstrap-treeview.css @@ -1,9 +1,9 @@ /* ========================================================= - * bootstrap-treeview.css v1.2.0 + * bootstrap-treeview.css v2.0.0 * ========================================================= - * Copyright 2013 Jonathan Miles + * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -34,4 +34,8 @@ .treeview .node-disabled { color: silver; cursor: not-allowed; -} \ No newline at end of file +} + +.treeview .node-hidden { + display: none; +} diff --git a/tests/lib/bootstrap-treeview.js b/tests/lib/bootstrap-treeview.js index 7a82a2eeb..820a1f941 100644 --- a/tests/lib/bootstrap-treeview.js +++ b/tests/lib/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.2.0 + * bootstrap-treeview.js v2.0.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -41,16 +41,15 @@ checkedIcon: 'glyphicon glyphicon-check', uncheckedIcon: 'glyphicon glyphicon-unchecked', - color: undefined, // '#000000', - backColor: undefined, // '#FFFFFF', - borderColor: undefined, // '#dddddd', + color: undefined, + backColor: undefined, + borderColor: undefined, onhoverColor: '#F5F5F5', selectedColor: '#FFFFFF', selectedBackColor: '#428bca', searchResultColor: '#D9534F', - searchResultBackColor: undefined, //'#FFFFFF', + searchResultBackColor: undefined, - enableLinks: false, highlightSelected: true, highlightSearchResults: true, showBorder: true, @@ -58,8 +57,16 @@ showCheckbox: false, showTags: false, multiSelect: false, + preventUnselect: false, // Event handlers + onLoading: undefined, + onLoadingFailed: undefined, + onInitialized: undefined, + onNodeRendered: undefined, + onRendered: undefined, + onDestroyed: undefined, + onNodeChecked: undefined, onNodeCollapsed: undefined, onNodeDisabled: undefined, @@ -68,6 +75,7 @@ onNodeSelected: undefined, onNodeUnchecked: undefined, onNodeUnselected: undefined, + onSearchComplete: undefined, onSearchCleared: undefined }; @@ -83,26 +91,32 @@ revealResults: true }; - var Tree = function (element, options) { + _default.dataUrl = { + method: 'GET', + dataType: 'json', + cache: false + }; + var Tree = function (element, options) { this.$element = $(element); - this.elementId = element.id; - this.styleId = this.elementId + '-style'; + this._elementId = element.id; + this._styleId = this._elementId + '-style'; - this.init(options); + this._init(options); return { // Options (public access) - options: this.options, + options: this._options, // Initialize / destroy methods - init: $.proxy(this.init, this), - remove: $.proxy(this.remove, this), + init: $.proxy(this._init, this), + remove: $.proxy(this._remove, this), - // Get methods - getNode: $.proxy(this.getNode, this), - getParent: $.proxy(this.getParent, this), + // Query methods + findNodes: $.proxy(this.findNodes, this), + getNodes: $.proxy(this.getNodes, this), // todo document + test + getParents: $.proxy(this.getParents, this), getSiblings: $.proxy(this.getSiblings, this), getSelected: $.proxy(this.getSelected, this), getUnselected: $.proxy(this.getUnselected, this), @@ -113,6 +127,13 @@ getDisabled: $.proxy(this.getDisabled, this), getEnabled: $.proxy(this.getEnabled, this), + // Tree manipulation methods + addNode: $.proxy(this.addNode, this), + addNodeAfter: $.proxy(this.addNodeAfter, this), + addNodeBefore: $.proxy(this.addNodeBefore, this), + removeNode: $.proxy(this.removeNode, this), + updateNode: $.proxy(this.updateNode, this), + // Select methods selectNode: $.proxy(this.selectNode, this), unselectNode: $.proxy(this.unselectNode, this), @@ -126,7 +147,7 @@ toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), revealNode: $.proxy(this.revealNode, this), - // Expand / collapse methods + // Check / uncheck methods checkAll: $.proxy(this.checkAll, this), checkNode: $.proxy(this.checkNode, this), uncheckAll: $.proxy(this.uncheckAll, this), @@ -146,48 +167,87 @@ }; }; - Tree.prototype.init = function (options) { - - this.tree = []; - this.nodes = []; + Tree.prototype._init = function (options) { + this._tree = []; + this._initialized = false; + + this._options = $.extend({}, _default.settings, options); + + this._destroy(); + this._subscribeEvents(); + + this._triggerEvent('loading', null, _default.options); + this._load(options) + .then($.proxy(function (data) { + // load done + return this._tree = $.extend(true, [], data); + }, this), $.proxy(function (error) { + // load fail + this._triggerEvent('loadingFailed', error, _default.options); + }, this)) + .then($.proxy(function (treeData) { + // initialize data + return this._setInitialStates({ nodes: treeData }, 0); + }, this)) + .then($.proxy(function () { + // render to DOM + this._render(); + }, this)); + }; + Tree.prototype._load = function (options) { + var done = new $.Deferred(); if (options.data) { - if (typeof options.data === 'string') { - options.data = $.parseJSON(options.data); - } - this.tree = $.extend(true, [], options.data); - delete options.data; + this._loadLocalData(options, done); + } else if (options.dataUrl) { + this._loadRemoteData(options, done); } - this.options = $.extend({}, _default.settings, options); + return done.promise(); + }; - this.destroy(); - this.subscribeEvents(); - this.setInitialStates({ nodes: this.tree }, 0); - this.render(); + Tree.prototype._loadRemoteData = function (options, done) { + $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl)) + .done(function (data) { + done.resolve(data); + }) + .fail(function (xhr, status, error) { + done.reject(error); + }); }; - Tree.prototype.remove = function () { - this.destroy(); - $.removeData(this, pluginName); - $('#' + this.styleId).remove(); + Tree.prototype._loadLocalData = function (options, done) { + done.resolve((typeof options.data === 'string') ? + $.parseJSON(options.data) : + $.extend(true, [], options.data)); }; - Tree.prototype.destroy = function () { + Tree.prototype._remove = function () { + this._destroy(); + $.removeData(this, pluginName); + $('#' + this._styleId).remove(); + }; - if (!this.initialized) return; + Tree.prototype._destroy = function () { + if (!this._initialized) return; + this._initialized = false; - this.$wrapper.remove(); - this.$wrapper = null; + this._triggerEvent('destroyed', null, _default.options); // Switch off events - this.unsubscribeEvents(); + this._unsubscribeEvents(); - // Reset this.initialized flag - this.initialized = false; + // Tear down + this.$wrapper.remove(); + this.$wrapper = null; }; - Tree.prototype.unsubscribeEvents = function () { - + Tree.prototype._unsubscribeEvents = function () { + this.$element.off('loading'); + this.$element.off('loadingFailed'); + this.$element.off('initialized'); + this.$element.off('nodeRendered'); + this.$element.off('rendered'); + this.$element.off('destroyed'); this.$element.off('click'); this.$element.off('nodeChecked'); this.$element.off('nodeCollapsed'); @@ -201,70 +261,118 @@ this.$element.off('searchCleared'); }; - Tree.prototype.subscribeEvents = function () { + Tree.prototype._subscribeEvents = function () { + this._unsubscribeEvents(); + + if (typeof (this._options.onLoading) === 'function') { + this.$element.on('loading', this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.$element.on('loadingFailed', this._options.onLoadingFailed); + } - this.unsubscribeEvents(); + if (typeof (this._options.onInitialized) === 'function') { + this.$element.on('initialized', this._options.onInitialized); + } - this.$element.on('click', $.proxy(this.clickHandler, this)); + if (typeof (this._options.onNodeRendered) === 'function') { + this.$element.on('nodeRendered', this._options.onNodeRendered); + } - if (typeof (this.options.onNodeChecked) === 'function') { - this.$element.on('nodeChecked', this.options.onNodeChecked); + if (typeof (this._options.onRendered) === 'function') { + this.$element.on('rendered', this._options.onRendered); } - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); + if (typeof (this._options.onDestroyed) === 'function') { + this.$element.on('destroyed', this._options.onDestroyed); } - if (typeof (this.options.onNodeDisabled) === 'function') { - this.$element.on('nodeDisabled', this.options.onNodeDisabled); + this.$element.on('click', $.proxy(this._clickHandler, this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.$element.on('nodeChecked', this._options.onNodeChecked); } - if (typeof (this.options.onNodeEnabled) === 'function') { - this.$element.on('nodeEnabled', this.options.onNodeEnabled); + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.$element.on('nodeCollapsed', this._options.onNodeCollapsed); } - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.on('nodeExpanded', this.options.onNodeExpanded); + if (typeof (this._options.onNodeDisabled) === 'function') { + this.$element.on('nodeDisabled', this._options.onNodeDisabled); } - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.on('nodeSelected', this.options.onNodeSelected); + if (typeof (this._options.onNodeEnabled) === 'function') { + this.$element.on('nodeEnabled', this._options.onNodeEnabled); } - if (typeof (this.options.onNodeUnchecked) === 'function') { - this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); + if (typeof (this._options.onNodeExpanded) === 'function') { + this.$element.on('nodeExpanded', this._options.onNodeExpanded); } - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.on('nodeUnselected', this.options.onNodeUnselected); + if (typeof (this._options.onNodeSelected) === 'function') { + this.$element.on('nodeSelected', this._options.onNodeSelected); } - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.on('searchComplete', this.options.onSearchComplete); + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.$element.on('nodeUnchecked', this._options.onNodeUnchecked); } - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.on('searchCleared', this.options.onSearchCleared); + if (typeof (this._options.onNodeUnselected) === 'function') { + this.$element.on('nodeUnselected', this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.$element.on('searchComplete', this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.$element.on('searchCleared', this._options.onSearchCleared); } }; + Tree.prototype._triggerEvent = function (event, data, options) { + if (options && !options.silent) { + this.$element.trigger(event, $.extend(true, {}, data)); + } + } + /* Recurse the tree structure and ensure all nodes have valid initial states. User defined states will be preserved. For performance we also take this opportunity to - index nodes in a flattened structure + index nodes in a flattened ordered structure */ - Tree.prototype.setInitialStates = function (node, level) { + Tree.prototype._setInitialStates = function (node, level) { + this._nodes = {}; + return $.when.apply(this, this._setInitialState(node, level)) + .done($.proxy(function () { + this._orderedNodes = this._sortNodes(); + this._triggerEvent('initialized', this._orderedNodes, _default.options); + return; + }, this)); + }; + Tree.prototype._setInitialState = function (node, level, done) { if (!node.nodes) return; level += 1; + done = done || []; var parent = node; - var _this = this; - $.each(node.nodes, function checkStates(index, node) { + $.each(node.nodes, $.proxy(function (index, node) { + var deferred = new $.Deferred(); + done.push(deferred.promise()); + + // level : hierarchical tree level, starts at 1 + node.level = level; - // nodeId : unique, incremental identifier - node.nodeId = _this.nodes.length; + // index : relative to siblings + node.index = index; + + // nodeId : unique, hierarchical identifier + node.nodeId = (parent && parent.nodeId) ? + parent.nodeId + '.' + node.index : + (level - 1) + '.' + node.index; // parentId : transversing up the tree node.parentId = parent.nodeId; @@ -290,7 +398,7 @@ // set expanded state; if not provided based on levels if (!node.state.hasOwnProperty('expanded')) { if (!node.state.disabled && - (level < _this.options.levels) && + (level < this._options.levels) && (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } @@ -304,431 +412,616 @@ node.state.selected = false; } - // index nodes in a flattened structure for use later - _this.nodes.push(node); + // set visible state; based parent state plus levels + if ((parent && parent.state && parent.state.expanded) || + (level <= this._options.levels)) { + node.state.visible = true; + } + else { + node.state.visible = false; + } - // recurse child nodes and transverse the tree + // recurse child nodes and transverse the tree, depth-first if (node.nodes) { - _this.setInitialStates(node, level); + if (node.nodes.length > 0) { + this._setInitialState(node, level, done); + } + else { + delete node.nodes; + } } - }); + + // add / update indexed collection + this._nodes[node.nodeId] = node; + + // mark task as complete + deferred.resolve(); + }, this)); + + return done; }; - Tree.prototype.clickHandler = function (event) { + Tree.prototype._sortNodes = function () { + return $.map(Object.keys(this._nodes).sort(), $.proxy(function (value, index) { + return this._nodes[value]; + }, this)); + }; - if (!this.options.enableLinks) event.preventDefault(); + Tree.prototype._clickHandler = function (event) { var target = $(event.target); - var node = this.findNode(target); + var node = this.targetNode(target); if (!node || node.state.disabled) return; - + var classList = target.attr('class') ? target.attr('class').split(' ') : []; if ((classList.indexOf('expand-icon') !== -1)) { - - this.toggleExpandedState(node, _default.options); - this.render(); + this._toggleExpanded(node, $.extend({}, _default.options)); } else if ((classList.indexOf('check-icon') !== -1)) { - - this.toggleCheckedState(node, _default.options); - this.render(); + this._toggleChecked(node, $.extend({}, _default.options)); } else { - if (node.selectable) { - this.toggleSelectedState(node, _default.options); + this._toggleSelected(node, $.extend({}, _default.options)); } else { - this.toggleExpandedState(node, _default.options); + this._toggleExpanded(node, $.extend({}, _default.options)); } - - this.render(); } }; // Looks up the DOM for the closest parent list item to retrieve the // data attribute nodeid, which is used to lookup the node in the flattened structure. - Tree.prototype.findNode = function (target) { - - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); - var node = this.nodes[nodeId]; - + Tree.prototype.targetNode = function (target) { + var nodeId = target.closest('li.list-group-item').attr('data-nodeId'); + var node = this._nodes[nodeId]; if (!node) { console.log('Error: node does not exist'); } return node; }; - Tree.prototype.toggleExpandedState = function (node, options) { + Tree.prototype._toggleExpanded = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, options); + this._setExpanded(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, options) { + Tree.prototype._setExpanded = function (node, state, options) { - if (state === node.state.expanded) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.expanded) return; if (state && node.nodes) { - // Expand a node + // Set node state node.state.expanded = true; - if (!options.silent) { - this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .addClass(this._options.collapseIcon); + } + + // Expand children + if (node.nodes && options) { + $.each(node.nodes, $.proxy(function (index, node) { + this._setVisible(node, true, options); + }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeExpanded', node, options); } else if (!state) { - // Collapse a node + // Set node state node.state.expanded = false; - if (!options.silent) { - this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.collapseIcon) + .addClass(this._options.expandIcon); } - // Collapse child nodes - if (node.nodes && !options.ignoreChildren) { + // Collapse children + if (node.nodes && options) { $.each(node.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, options); + this._setVisible(node, false, options); + this._setExpanded(node, false, options); }, this)); } + + // Optionally trigger event + this._triggerEvent('nodeCollapsed', node, options); } }; - Tree.prototype.toggleSelectedState = function (node, options) { + Tree.prototype._setVisible = function (node, state, options) { + + if (options && state === node.state.visible) return; + + if (state) { + + // Set node state + node.state.visible = true; + + // Set element + if (node.$el) { + node.$el.removeClass('node-hidden'); + } + } + else { + + // Set node state to unchecked + node.state.visible = false; + + // Set element + if (node.$el) { + node.$el.addClass('node-hidden'); + } + } + }; + + Tree.prototype._toggleSelected = function (node, options) { if (!node) return; - this.setSelectedState(node, !node.state.selected, options); + this._setSelected(node, !node.state.selected, options); + return this; }; - Tree.prototype.setSelectedState = function (node, state, options) { + Tree.prototype._setSelected = function (node, state, options) { - if (state === node.state.selected) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && (state === node.state.selected)) return; if (state) { // If multiSelect false, unselect previously selected - if (!this.options.multiSelect) { - $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, options); + if (!this._options.multiSelect) { + $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) { + this._setSelected(node, false, $.extend(options, {unselecting: true})); }, this)); } - // Continue selecting node + // Set node state node.state.selected = true; - if (!options.silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.addClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.icon || this._options.nodeIcon) + .addClass(node.selectedIcon || this._options.selectedIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeSelected', node, options); } else { - // Unselect node + // If preventUnselect true + only one remaining selection, disable unselect + if (this._options.preventUnselect && + (options && !options.unselecting) && + (this._findNodes('true', 'state.selected').length === 1)) { + return this; + } + + // Set node state node.state.selected = false; - if (!options.silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.selectedIcon || this._options.selectedIcon) + .addClass(node.icon || this._options.nodeIcon); + } } + + // Optionally trigger event + this._triggerEvent('nodeUnselected', node, options); } + + return this; }; - Tree.prototype.toggleCheckedState = function (node, options) { + Tree.prototype._toggleChecked = function (node, options) { if (!node) return; - this.setCheckedState(node, !node.state.checked, options); + this._setChecked(node, !node.state.checked, options); }; - Tree.prototype.setCheckedState = function (node, state, options) { + Tree.prototype._setChecked = function (node, state, options) { - if (state === node.state.checked) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.checked) return; if (state) { - // Check node + // Set node state node.state.checked = true; - if (!options.silent) { - this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .addClass(this._options.checkedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); } else { - // Uncheck node + // Set node state to unchecked node.state.checked = false; - if (!options.silent) { - this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.checkedIcon) + .addClass(this._options.uncheckedIcon); } + + // Optionally trigger event + this._triggerEvent('nodeUnchecked', node, options); } }; - Tree.prototype.setDisabledState = function (node, state, options) { + Tree.prototype._setDisabled = function (node, state, options) { - if (state === node.state.disabled) return; + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.disabled) return; if (state) { - // Disable node + // Set node state to disabled node.state.disabled = true; // Disable all other states - this.setExpandedState(node, false, options); - this.setSelectedState(node, false, options); - this.setCheckedState(node, false, options); + this._setSelected(node, false, options); + this._setChecked(node, false, options); + this._setExpanded(node, false, options); - if (!options.silent) { - this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + // Set element + if (node.$el) { + node.$el.addClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeDisabled', node, options); } else { - // Enabled node + // Set node state to enabled node.state.disabled = false; - if (!options.silent) { - this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); + + // Set element + if (node.$el) { + node.$el.removeClass('node-disabled'); } + + // Optionally trigger event + this._triggerEvent('nodeEnabled', node, options); } }; - Tree.prototype.render = function () { + Tree.prototype._setSearchResult = function (node, state, options) { + if (options && state === node.searchResult) return; - if (!this.initialized) { - - // Setup first time only components - this.$element.addClass(pluginName); - this.$wrapper = $(this.template.list); + if (state) { - this.injectStyle(); + node.searchResult = true; - this.initialized = true; + if (node.$el) { + node.$el.addClass('node-result'); + } } + else { - this.$element.empty().append(this.$wrapper.empty()); + node.searchResult = false; - // Build tree - this.buildTree(this.tree, 0); + if (node.$el) { + node.$el.removeClass('node-result'); + } + } }; - // Starting from the root node, and recursing down the - // structure we build the tree one node at a time - Tree.prototype.buildTree = function (nodes, level) { + Tree.prototype._render = function () { + if (!this._initialized) { - if (!nodes) return; - level += 1; + // Setup first time only components + this.$wrapper = $(this._template.tree); + this.$element.empty() + .addClass(pluginName) + .append(this.$wrapper); - var _this = this; - $.each(nodes, function addNodes(id, node) { - - var treeItem = $(_this.template.item) - .addClass('node-' + _this.elementId) - .addClass(node.state.checked ? 'node-checked' : '') - .addClass(node.state.disabled ? 'node-disabled': '') - .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') - .attr('data-nodeid', node.nodeId) - .attr('style', _this.buildStyleOverride(node)); - - // Add indent/spacer to mimic tree structure - for (var i = 0; i < (level - 1); i++) { - treeItem.append(_this.template.indent); - } + this._injectStyle(); - // Add expand, collapse or empty spacer icons - var classList = []; - if (node.nodes) { - classList.push('expand-icon'); - if (node.state.expanded) { - classList.push(_this.options.collapseIcon); - } - else { - classList.push(_this.options.expandIcon); - } - } - else { - classList.push(_this.options.emptyIcon); - } + this._initialized = true; + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); + var previousNode; + $.each(this._orderedNodes, $.proxy(function (id, node) { + this._renderNode(node, previousNode); + previousNode = node; + }, this)); + this._triggerEvent('rendered', this._orderedNodes, _default.options); + }; - // Add node icon - if (_this.options.showIcon) { - - var classList = ['node-icon']; + Tree.prototype._renderNode = function (node, previousNode) { + if (!node) return; - classList.push(node.icon || _this.options.nodeIcon); - if (node.state.selected) { - classList.pop(); - classList.push(node.selectedIcon || _this.options.selectedIcon || - node.icon || _this.options.nodeIcon); - } + if (!node.$el) { + node.$el = this._newNodeEl(node, previousNode) + .addClass('node-' + this._elementId); + } + else { + node.$el.empty(); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Set / update nodeid; it can change as a result of addNode etc. + node.$el.attr('data-nodeId', node.nodeId); - // Add check / unchecked icon - if (_this.options.showCheckbox) { + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (node.level - 1); i++) { + node.$el.append(this._template.indent); + } - var classList = ['check-icon']; - if (node.state.checked) { - classList.push(_this.options.checkedIcon); - } - else { - classList.push(_this.options.uncheckedIcon); - } + // Add expand / collapse or empty spacer icons + node.$el + .append($(this._template.icon) + .addClass(node.nodes ? 'expand-icon' : this._options.emptyIcon) + ); + + // Add node icon + if (this._options.showIcon) { + node.$el + .append($(this._template.icon) + .addClass('node-icon') + .addClass(node.icon || this._options.nodeIcon) + ); + } - treeItem - .append($(_this.template.icon) - .addClass(classList.join(' ')) - ); - } + // Add checkable icon + if (this._options.showCheckbox) { + node.$el + .append($(this._template.icon) + .addClass('check-icon') + ); + } - // Add text - if (_this.options.enableLinks) { - // Add hyperlink - treeItem - .append($(_this.template.link) - .attr('href', node.href) - .append(node.text) - ); - } - else { - // otherwise just text - treeItem - .append(node.text); - } + // Add text + node.$el.append(node.text); - // Add tags as badges - if (_this.options.showTags && node.tags) { - $.each(node.tags, function addTag(id, tag) { - treeItem - .append($(_this.template.badge) - .append(tag) - ); - }); - } + // Add tags as badges + if (this._options.showTags && node.tags) { + $.each(node.tags, $.proxy(function addTag(id, tag) { + node.$el + .append($(this._template.badge) + .append(tag) + ); + }, this)); + } - // Add item to the tree - _this.$wrapper.append(treeItem); + // Set various node states + this._setSelected(node, node.state.selected); + this._setChecked(node, node.state.checked); + this._setSearchResult(node, node.searchResult); + this._setExpanded(node, node.state.expanded); + this._setDisabled(node, node.state.disabled); + this._setVisible(node, node.state.visible); + + // Trigger nodeRendered event + this._triggerEvent('nodeRendered', node, _default.options); + }; + + // Creates a new node element from template and + // ensures the template is inserted at the correct position + Tree.prototype._newNodeEl = function (node, previousNode) { + var $el = $(this._template.node); + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + this.$wrapper.children() + .eq(previousNode.$el.index()) + .after($el); + } else { + // we use prepend instead of append, + // to cater for root inserts i.e. nodeId 0.0 + this.$wrapper.prepend($el); + } - // Recursively add child ndoes - if (node.nodes && node.state.expanded && !node.state.disabled) { - return _this.buildTree(node.nodes, level); - } - }); + return $el; }; - // Define any node level style override for - // 1. selectedNode - // 2. node|data assigned color overrides - Tree.prototype.buildStyleOverride = function (node) { - - if (node.state.disabled) return ''; - - var color = node.color; - var backColor = node.backColor; + // Recursively remove node elements from DOM + Tree.prototype._removeNodeEl = function (node) { + if (!node) return; - if (this.options.highlightSelected && node.state.selected) { - if (this.options.selectedColor) { - color = this.options.selectedColor; - } - if (this.options.selectedBackColor) { - backColor = this.options.selectedBackColor; - } + if (node.nodes) { + $.each(node.nodes, $.proxy(function (index, node) { + this._removeNodeEl(node); + }, this)); } + node.$el.remove(); + }; - if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { - if (this.options.searchResultColor) { - color = this.options.searchResultColor; - } - if (this.options.searchResultBackColor) { - backColor = this.options.searchResultBackColor; - } - } + // Expand node, rendering it's immediate children + Tree.prototype._expandNode = function (node) { + if (!node.nodes) return; - return 'color:' + color + - ';background-color:' + backColor + ';'; + $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) { + childNode.level = node.level + 1; + this._renderNode(childNode, node.$el); + }, this)); }; // Add inline style into head - Tree.prototype.injectStyle = function () { - - if (this.options.injectStyle && !document.getElementById(this.styleId)) { - $('').appendTo('head'); + Tree.prototype._injectStyle = function () { + if (this._options.injectStyle && !document.getElementById(this._styleId)) { + $('').appendTo('head'); } }; // Construct trees style based on user options - Tree.prototype.buildStyle = function () { - - var style = '.node-' + this.elementId + '{'; + Tree.prototype._buildStyle = function () { + var style = '.node-' + this._elementId + '{'; - if (this.options.color) { - style += 'color:' + this.options.color + ';'; + // Basic bootstrap style overrides + if (this._options.color) { + style += 'color:' + this._options.color + ';'; } - if (this.options.backColor) { - style += 'background-color:' + this.options.backColor + ';'; + if (this._options.backColor) { + style += 'background-color:' + this._options.backColor + ';'; } - if (!this.options.showBorder) { + if (!this._options.showBorder) { style += 'border:none;'; } - else if (this.options.borderColor) { - style += 'border:1px solid ' + this.options.borderColor + ';'; + else if (this._options.borderColor) { + style += 'border:1px solid ' + this._options.borderColor + ';'; } style += '}'; - if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + if (this._options.onhoverColor) { + style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this._options.onhoverColor + ';' + '}'; } - return this.css + style; + // Style search results + if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { + + var innerStyle = '' + if (this._options.searchResultColor) { + innerStyle += 'color:' + this._options.searchResultColor + ';'; + } + if (this._options.searchResultBackColor) { + innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; + } + + // Style selected nodes + if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { + + var innerStyle = '' + if (this._options.selectedColor) { + innerStyle += 'color:' + this._options.selectedColor + ';'; + } + if (this._options.selectedBackColor) { + innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; + } + + // Node level style overrides + $.each(this._orderedNodes, $.proxy(function (index, node) { + if (node.color || node.backColor) { + var innerStyle = ''; + if (node.color) { + innerStyle += 'color:' + node.color + ';'; + } + if (node.backColor) { + innerStyle += 'background-color:' + node.backColor + ';'; + } + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; + } + }, this)); + + return this._css + style; }; - Tree.prototype.template = { - list: '
                ', - item: '
              • ', + Tree.prototype._template = { + tree: '
                  ', + node: '
                • ', indent: '', icon: '', - link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + Tree.prototype._css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + Returns an array of matching node objects. + @param {String} pattern - A pattern to match against a given field + @return {String} field - Field to query pattern against + */ + Tree.prototype.findNodes = function (pattern, field) { + return this._findNodes(pattern, field); + }; /** - Returns a single node object that matches the given node id. - @param {Number} nodeId - A node's unique identifier - @return {Object} node - Matching node + Returns an ordered aarray of node objects. + @return {Array} nodes - An array of all nodes */ - Tree.prototype.getNode = function (nodeId) { - return this.nodes[nodeId]; + Tree.prototype.getNodes = function () { + return this._orderedNodes; }; /** - Returns the parent node of a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Object} node - The parent node + Returns parent nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of parent nodes */ - Tree.prototype.getParent = function (identifier) { - var node = this.identifyNode(identifier); - return this.nodes[node.parentId]; + Tree.prototype.getParents = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var parentNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node.parentId ? this._nodes[node.parentId] : false; + if (parentNode) { + parentNodes.push(parentNode); + } + }, this)); + return parentNodes; }; /** - Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. - @param {Object|Number} identifier - A valid node or node id - @returns {Array} nodes - Sibling nodes + Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of sibling nodes */ - Tree.prototype.getSiblings = function (identifier) { - var node = this.identifyNode(identifier); - var parent = this.getParent(node); - var nodes = parent ? parent.nodes : this.tree; - return nodes.filter(function (obj) { + Tree.prototype.getSiblings = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var siblingNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parent = this.getParents([node]); + var nodes = parent[0] ? parent[0].nodes : this._tree; + siblingNodes = nodes.filter(function (obj) { return obj.nodeId !== node.nodeId; }); + }, this)); + + // flatten possible nested array before returning + return $.map(siblingNodes, function (obj) { + return obj; + }); }; /** @@ -736,7 +1029,7 @@ @returns {Array} nodes - Selected nodes */ Tree.prototype.getSelected = function () { - return this.findNodes('true', 'g', 'state.selected'); + return this._findNodes('true', 'state.selected'); }; /** @@ -744,7 +1037,7 @@ @returns {Array} nodes - Unselected nodes */ Tree.prototype.getUnselected = function () { - return this.findNodes('false', 'g', 'state.selected'); + return this._findNodes('false', 'state.selected'); }; /** @@ -752,7 +1045,7 @@ @returns {Array} nodes - Expanded nodes */ Tree.prototype.getExpanded = function () { - return this.findNodes('true', 'g', 'state.expanded'); + return this._findNodes('true', 'state.expanded'); }; /** @@ -760,7 +1053,7 @@ @returns {Array} nodes - Collapsed nodes */ Tree.prototype.getCollapsed = function () { - return this.findNodes('false', 'g', 'state.expanded'); + return this._findNodes('false', 'state.expanded'); }; /** @@ -768,7 +1061,7 @@ @returns {Array} nodes - Checked nodes */ Tree.prototype.getChecked = function () { - return this.findNodes('true', 'g', 'state.checked'); + return this._findNodes('true', 'state.checked'); }; /** @@ -776,7 +1069,7 @@ @returns {Array} nodes - Unchecked nodes */ Tree.prototype.getUnchecked = function () { - return this.findNodes('false', 'g', 'state.checked'); + return this._findNodes('false', 'state.checked'); }; /** @@ -784,7 +1077,7 @@ @returns {Array} nodes - Disabled nodes */ Tree.prototype.getDisabled = function () { - return this.findNodes('true', 'g', 'state.disabled'); + return this._findNodes('true', 'state.disabled'); }; /** @@ -792,47 +1085,208 @@ @returns {Array} nodes - Enabled nodes */ Tree.prototype.getEnabled = function () { - return this.findNodes('false', 'g', 'state.disabled'); + return this._findNodes('false', 'state.disabled'); }; /** - Set a node state to selected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Add nodes to the tree. + @param {Array} nodes - An array of nodes to add + @param {optional Object} parentNode - The node to which nodes will be added as children + @param {optional number} index - Zero based insert index @param {optional Object} options */ - Tree.prototype.selectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, true, options); + Tree.prototype.addNode = function (nodes, parentNode, index, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (parentNode instanceof Array) { + parentNode = parentNode[0]; + } + + options = $.extend({}, _default.options, options); + + // identify target nodes; either the tree's root or a parent's child nodes + var targetNodes; + if (parentNode && parentNode.nodes) { + targetNodes = parentNode.nodes; + } else if (parentNode) { + targetNodes = parentNode.nodes = []; + } else { + targetNodes = this._tree; + } + + // inserting nodes at specified positions + $.each(nodes, $.proxy(function (i, node) { + var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); + targetNodes.splice(insertIndex, 0, node); }, this)); - this.render(); + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done($.proxy(function () { + if (parentNode && !parentNode.state.expanded) { + this._setExpanded(parentNode, true, options); + } + this._render(); + }, this)); + } + + /** + Add nodes to the tree after given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added after + @param {optional Object} options + */ + Tree.prototype.addNodeAfter = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); + } + + /** + Add nodes to the tree before given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added before + @param {optional Object} options + */ + Tree.prototype.addNodeBefore = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], node.index, options); + } + + /** + Removes given nodes from the tree. + @param {Array} nodes - An array of nodes to remove + @param {optional Object} options + */ + Tree.prototype.removeNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + var targetNodes, parentNode; + $.each(nodes, $.proxy(function (index, node) { + + // remove nodes from tree + parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1); + + // remove node from DOM + this._removeNodeEl(node); + }, this)); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); + }; + + /** + Updates / replaces a given tree node + @param {Object} node - A single node to be replaced + @param {Object} newNode - THe replacement node + @param {optional Object} options + */ + Tree.prototype.updateNode = function (node, newNode, options) { + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + // insert new node + var targetNodes; + var parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1, newNode); + + // remove old node from DOM + this._removeNodeEl(node); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); }; + /** - Set a node state to unselected - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Selects given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setSelectedState(node, false, options); + Tree.prototype.selectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, true, options); }, this)); + }; - this.render(); + /** + Unselects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.unselectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, false, options); + }, this)); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleSelectedState(node, options); - }, this)); + Tree.prototype.toggleNodeSelected = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleSelected(node, options); + }, this)); }; @@ -841,25 +1295,22 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); - - this.render(); + options = $.extend({}, _default.options, options); + options.levels = options.levels || 999; + this.collapseNode(this._tree, options); }; /** Collapse a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, false, options); - }, this)); + Tree.prototype.collapseNode = function (nodes, options) { + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, false, options); + }, this)); }; /** @@ -868,75 +1319,82 @@ */ Tree.prototype.expandAll = function (options) { options = $.extend({}, _default.options, options); - - if (options && options.levels) { - this.expandLevels(this.tree, options.levels, options); - } - else { - var identifiers = this.findNodes('false', 'g', 'state.expanded'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - }, this)); - } - - this.render(); + options.levels = options.levels || 999; + this.expandNode(this._tree, options); }; /** - Expand a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Expand given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.expandNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setExpandedState(node, true, options); - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, options); + Tree.prototype.expandNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, true, options); + if (node.nodes) { + this._expandLevels(node.nodes, options.levels-1, options); } }, this)); - - this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, options) { + Tree.prototype._expandLevels = function (nodes, level, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false, options); + this._setExpanded(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, options); + this._expandLevels(node.nodes, level-1, options); } }, this)); }; /** - Reveals a given tree node, expanding the tree from node to root. - @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + Reveals given tree nodes, expanding the tree from node to root. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.revealNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - var parentNode = this.getParent(node); - while (parentNode) { - this.setExpandedState(parentNode, true, options); - parentNode = this.getParent(parentNode); + Tree.prototype.revealNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node; + var tmpNode; + while (tmpNode = this.getParents([parentNode])[0]) { + parentNode = tmpNode; + this._setExpanded(parentNode, true, options); }; }, this)); - - this.render(); }; /** - Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleExpandedState(node, options); + Tree.prototype.toggleNodeExpanded = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleExpanded(node, options); }, this)); - - this.render(); }; @@ -945,25 +1403,29 @@ @param {optional Object} options */ Tree.prototype.checkAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** - Check a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Checks given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.checkNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, true, options); - }, this)); + Tree.prototype.checkNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); }; /** @@ -971,38 +1433,46 @@ @param {optional Object} options */ Tree.prototype.uncheckAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.checked'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.checked'); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Uncheck a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Uncheck given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.uncheckNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setCheckedState(node, false, options); - }, this)); + Tree.prototype.uncheckNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); }; /** - Toggles a nodes checked state; checking if unchecked, unchecking if checked. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Toggles a node's checked state; checking if unchecked, unchecking if checked. + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.toggleNodeChecked = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.toggleCheckedState(node, options); - }, this)); + Tree.prototype.toggleNodeChecked = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._toggleChecked(node, options); + }, this)); }; @@ -1011,25 +1481,29 @@ @param {optional Object} options */ Tree.prototype.disableAll = function (options) { - var identifiers = this.findNodes('false', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('false', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** - Disable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Disable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.disableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, true, options); - }, this)); + Tree.prototype.disableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); - this.render(); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); }; /** @@ -1037,65 +1511,48 @@ @param {optional Object} options */ Tree.prototype.enableAll = function (options) { - var identifiers = this.findNodes('true', 'g', 'state.disabled'); - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + options = $.extend({}, _default.options, options); - this.render(); + var nodes = this._findNodes('true', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); + }, this)); }; /** - Enable a given tree node - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + Enable given tree nodes + @param {Array} nodes - An array of nodes @param {optional Object} options */ - Tree.prototype.enableNode = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, false, options); - }, this)); + Tree.prototype.enableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } - this.render(); - }; + options = $.extend({}, _default.options, options); - /** - Toggles a nodes disabled state; disabling is enabled, enabling if disabled. - @param {Object|Number} identifiers - A valid node, node id or array of node identifiers - @param {optional Object} options - */ - Tree.prototype.toggleNodeDisabled = function (identifiers, options) { - this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { - this.setDisabledState(node, !node.state.disabled, options); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); }, this)); - - this.render(); }; - /** - Common code for processing multiple identifiers + Toggles a node's disabled state; disabling is enabled, enabling if disabled. + @param {Array} nodes - An array of nodes + @param {optional Object} options */ - Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + Tree.prototype.toggleNodeDisabled = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } options = $.extend({}, _default.options, options); - if (!(identifiers instanceof Array)) { - identifiers = [identifiers]; - } - - $.each(identifiers, $.proxy(function (index, identifier) { - callback(this.identifyNode(identifier), options); - }, this)); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, !node.state.disabled, options); + }, this)); }; - /* - Identifies a node from either a node id or object - */ - Tree.prototype.identifyNode = function (identifier) { - return ((typeof identifier) === 'number') ? - this.nodes[identifier] : - identifier; - }; /** Searches the tree for nodes (text) that match given criteria @@ -1106,9 +1563,9 @@ Tree.prototype.search = function (pattern, options) { options = $.extend({}, _default.searchOptions, options); - this.clearSearch({ render: false }); - + var previous = this._getSearchResults(); var results = []; + if (pattern && pattern.length > 0) { if (options.exactMatch) { @@ -1120,26 +1577,25 @@ modifier += 'i'; } - results = this.findNodes(pattern, modifier); - - // Add searchResult property to all matching nodes - // This will be used to apply custom styles - // and when identifying result to be cleared - $.each(results, function (index, node) { - node.searchResult = true; - }) + results = this._findNodes(pattern, 'text', modifier); } - // If revealResults, then render is triggered from revealNode - // otherwise we just call render. - if (options.revealResults) { + // Clear previous results no longer matched + $.each(this._diffArray(results, previous), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); + + // Set new results + $.each(this._diffArray(previous, results), $.proxy(function (index, node) { + this._setSearchResult(node, true, options); + }, this)); + + // Reveal hidden nodes + if (results && options.revealResults) { this.revealNode(results); } - else { - this.render(); - } - this.$element.trigger('searchComplete', $.extend(true, {}, results)); + this._triggerEvent('searchComplete', results, options); return results; }; @@ -1148,39 +1604,45 @@ Clears previous search results */ Tree.prototype.clearSearch = function (options) { - options = $.extend({}, { render: true }, options); - var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { - node.searchResult = false; - }); + var results = $.each(this._getSearchResults(), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); - if (options.render) { - this.render(); - } - - this.$element.trigger('searchCleared', $.extend(true, {}, results)); + this._triggerEvent('searchCleared', results, options); + }; + + Tree.prototype._getSearchResults = function () { + return this._findNodes('true', 'searchResult'); + }; + + Tree.prototype._diffArray = function (a, b) { + var diff = []; + $.grep(b, function (n) { + if ($.inArray(n, a) === -1) { + diff.push(n); + } + }); + return diff; }; /** Find nodes that match a given criteria @param {String} pattern - A given string to match against - @param {optional String} modifier - Valid RegEx modifiers @param {optional String} attribute - Attribute to compare pattern against + @param {optional String} modifier - Valid RegEx modifiers @return {Array} nodes - Nodes that match your criteria */ - Tree.prototype.findNodes = function (pattern, modifier, attribute) { - - modifier = modifier || 'g'; + Tree.prototype._findNodes = function (pattern, attribute, modifier) { attribute = attribute || 'text'; - - var _this = this; - return $.grep(this.nodes, function (node) { - var val = _this.getNodeValue(node, attribute); + modifier = modifier || 'g'; + return $.grep(this._orderedNodes, $.proxy(function (node) { + var val = this._getNodeValue(node, attribute); if (typeof val === 'string') { return val.match(new RegExp(pattern, modifier)); } - }); + }, this)); }; /** @@ -1190,12 +1652,12 @@ @param {String} attr - Identifies an object property using dot notation @return {String} value - Matching attributes string representation */ - Tree.prototype.getNodeValue = function (obj, attr) { + Tree.prototype._getNodeValue = function (obj, attr) { var index = attr.indexOf('.'); if (index > 0) { var _obj = obj[attr.substring(0, index)]; var _attr = attr.substring(index + 1, attr.length); - return this.getNodeValue(_obj, _attr); + return this._getNodeValue(_obj, _attr); } else { if (obj.hasOwnProperty(attr)) { diff --git a/tests/lib/jquery.js b/tests/lib/jquery.js index ebc6c187d..eed17778c 100644 --- a/tests/lib/jquery.js +++ b/tests/lib/jquery.js @@ -1,81 +1,84 @@ /*! - * jQuery JavaScript Library v2.0.3 + * jQuery JavaScript Library v2.1.4 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * - * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-07-03T13:30Z + * Date: 2015-04-28T16:01Z */ -(function( window, undefined ) { -// Can't do this because several apps including ASP.NET trace +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if // you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -//"use strict"; -var - // A central reference to the root jQuery(document) - rootjQuery, +// - // The deferred used on DOM ready - readyList, +var arr = []; - // Support: IE9 - // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` - core_strundefined = typeof undefined, +var slice = arr.slice; - // Use the correct document accordingly with window argument (sandbox) - location = window.location, - document = window.document, - docElem = document.documentElement, +var concat = arr.concat; - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, +var push = arr.push; - // Map over the $ in case of overwrite - _$ = window.$, +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; - // [[Class]] -> type pairs - class2type = {}, +var hasOwn = class2type.hasOwnProperty; - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], +var support = {}; - core_version = "2.0.3", - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, + +var + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + version = "2.1.4", // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); }, - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, // Matches dashed string for camelizing rmsPrefix = /^-ms-/, @@ -84,114 +87,13 @@ var // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); - }, - - // The ready event handler and self cleanup method - completed = function() { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - jQuery.ready(); }; jQuery.fn = jQuery.prototype = { // The current version of jQuery being used - jquery: core_version, + jquery: version, constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, // Start with an empty selector selector: "", @@ -200,19 +102,19 @@ jQuery.fn = jQuery.prototype = { length: 0, toArray: function() { - return core_slice.call( this ); + return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { - return num == null ? + return num != null ? - // Return a 'clean' array - this.toArray() : + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); + // Return all the elements in a clean array + slice.call( this ); }, // Take an array of elements and push it onto the stack @@ -237,15 +139,14 @@ jQuery.fn = jQuery.prototype = { return jQuery.each( this, callback, args ); }, - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); }, slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); + return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { @@ -262,26 +163,17 @@ jQuery.fn = jQuery.prototype = { return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); }, - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice + push: push, + sort: arr.sort, + splice: arr.splice }; -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, @@ -292,9 +184,10 @@ jQuery.extend = jQuery.fn.extend = function() { // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; } // Handle case when target is a string or something (possible in deep copy) @@ -302,10 +195,10 @@ jQuery.extend = jQuery.fn.extend = function() { target = {}; } - // extend jQuery itself if only one argument is passed - if ( length === i ) { + // Extend jQuery itself if only one argument is passed + if ( i === length ) { target = this; - --i; + i--; } for ( ; i < length; i++ ) { @@ -348,64 +241,17 @@ jQuery.extend = jQuery.fn.extend = function() { jQuery.extend({ // Unique for each copy of jQuery on the page - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), - - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, + // Assume jQuery is ready without the ready module + isReady: true, - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } + error: function( msg ) { + throw new Error( msg ); }, - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, + noop: function() {}, - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { return jQuery.type(obj) === "function"; }, @@ -417,17 +263,11 @@ jQuery.extend({ }, isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - // Support: Safari <= 5.1 (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0; }, isPlainObject: function( obj ) { @@ -439,16 +279,8 @@ jQuery.extend({ return false; } - // Support: Firefox <20 - // The try/catch suppresses exceptions thrown when attempting to access - // the "constructor" property of certain host objects, ie. |window.location| - // https://bugzilla.mozilla.org/show_bug.cgi?id=814622 - try { - if ( obj.constructor && - !core_hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { - return false; - } - } catch ( e ) { + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { return false; } @@ -465,69 +297,20 @@ jQuery.extend({ return true; }, - error: function( msg ) { - throw new Error( msg ); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - - if ( scripts ) { - jQuery( scripts ).remove(); - } - - return jQuery.merge( [], parsed.childNodes ); - }, - - parseJSON: JSON.parse, - - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE9 - try { - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } catch ( e ) { - xml = undefined; - } - - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); + type: function( obj ) { + if ( obj == null ) { + return obj + ""; } - return xml; + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; }, - noop: function() {}, - // Evaluates a script in a global context globalEval: function( code ) { var script, - indirect = eval; + indirect = eval; code = jQuery.trim( code ); @@ -548,6 +331,7 @@ jQuery.extend({ }, // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); @@ -607,8 +391,11 @@ jQuery.extend({ return obj; }, + // Support: Android<4.1 trim: function( text ) { - return text == null ? "" : core_trim.call( text ); + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); }, // results is for internal usage only @@ -622,7 +409,7 @@ jQuery.extend({ [ arr ] : arr ); } else { - core_push.call( ret, arr ); + push.call( ret, arr ); } } @@ -630,22 +417,16 @@ jQuery.extend({ }, inArray: function( elem, arr, i ) { - return arr == null ? -1 : core_indexOf.call( arr, elem, i ); + return arr == null ? -1 : indexOf.call( arr, elem, i ); }, merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; + var len = +second.length, + j = 0, + i = first.length; - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; } first.length = i; @@ -653,23 +434,23 @@ jQuery.extend({ return first; }, - grep: function( elems, callback, inv ) { - var retVal, - ret = [], + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], i = 0, - length = elems.length; - inv = !!inv; + length = elems.length, + callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); } } - return ret; + return matches; }, // arg is for internal usage only @@ -680,13 +461,13 @@ jQuery.extend({ isArray = isArraylike( elems ), ret = []; - // Go through the array, translating each of the items to their + // Go through the array, translating each of the items to their new values if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } @@ -696,13 +477,13 @@ jQuery.extend({ value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } } // Flatten any nested arrays - return core_concat.apply( [], ret ); + return concat.apply( [], ret ); }, // A global GUID counter for objects @@ -726,9 +507,9 @@ jQuery.extend({ } // Simulated bind - args = core_slice.call( arguments, 2 ); + args = slice.call( arguments, 2 ); proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed @@ -737,154 +518,62 @@ jQuery.extend({ return proxy; }, - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } + now: Date.now, - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +}); - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } +function isArraylike( obj ) { - return chainable ? - elems : + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = "length" in obj && obj.length, + type = jQuery.type( obj ); - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } - now: Date.now, + if ( obj.nodeType === 1 && length ) { + return true; + } - // A method for quickly swapping in/out CSS properties to get correct calculations. - // Note: this method belongs to the css module but it's needed here for the support module. - // If support gets modularized, this method should be moved back to the css module. - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } -}); - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - } else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - } - } - return readyList.promise( obj ); -}; - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - var length = obj.length, - type = jQuery.type( obj ); - - if ( jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; } - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); +var Sizzle = /*! - * Sizzle CSS Selector Engine v1.9.4-pre + * Sizzle CSS Selector Engine v2.2.0-pre * http://sizzlejs.com/ * - * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-06-03 + * Date: 2014-12-16 */ -(function( window, undefined ) { +(function( window ) { var i, support, - cachedruns, Expr, getText, isXML, + tokenize, compile, + select, outermostContext, sortInput, + hasDuplicate, // Local document vars setDocument, @@ -897,24 +586,21 @@ var i, contains, // Instance-specific data - expando = "sizzle" + -(new Date()), + expando = "sizzle" + 1 * new Date(), preferredDoc = window.document, dirruns = 0, done = 0, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), - hasDuplicate = false, sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; - return 0; } return 0; }, // General-purpose constants - strundefined = typeof undefined, MAX_NEGATIVE = 1 << 31, // Instance methods @@ -924,12 +610,13 @@ var i, push_native = arr.push, push = arr.push, slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { var i = 0, - len = this.length; + len = list.length; for ( ; i < len; i++ ) { - if ( this[i] === elem ) { + if ( list[i] === elem ) { return i; } } @@ -950,26 +637,32 @@ var i, // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = characterEncoding.replace( "w", "w#" ), - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + characterEncoding + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - rsibling = new RegExp( whitespace + "*[+~]" ), - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), @@ -990,14 +683,15 @@ var i, whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - + rsibling = /[+~]/, rescape = /'|\\/g, // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters @@ -1005,15 +699,23 @@ var i, funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint - // Support: Firefox + // Support: Firefox<24 // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : - // BMP codepoint high < 0 ? + // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); }; // Optimize for push.apply( _, NodeList ) @@ -1056,25 +758,24 @@ function Sizzle( selector, context, results, seed ) { context = context || document; results = results || []; + nodeType = context.nodeType; - if ( !selector || typeof selector !== "string" ) { - return results; - } + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; + return results; } - if ( documentIsHTML && !seed ) { + if ( !seed && documentIsHTML ) { - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { + // Try to shortcut find operations when possible (e.g., not under DocumentFragment) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { // Speed-up: Sizzle("#ID") if ( (m = match[1]) ) { if ( nodeType === 9 ) { elem = context.getElementById( m ); // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 + // nodes that are no longer in the document (jQuery #6963) if ( elem && elem.parentNode ) { // Handle the case where IE, Opera, and Webkit return items // by name instead of ID @@ -1100,7 +801,7 @@ function Sizzle( selector, context, results, seed ) { return results; // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + } else if ( (m = match[3]) && support.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); return results; } @@ -1110,7 +811,7 @@ function Sizzle( selector, context, results, seed ) { if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { nid = old = expando; newContext = context; - newSelector = nodeType === 9 && selector; + newSelector = nodeType !== 1 && selector; // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root @@ -1130,7 +831,7 @@ function Sizzle( selector, context, results, seed ) { while ( i-- ) { groups[i] = nid + toSelector( groups[i] ); } - newContext = rsibling.test( selector ) && context.parentNode || context; + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; newSelector = groups.join(","); } @@ -1165,11 +866,11 @@ function createCache() { function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { + if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } - return (cache[ key ] = value); + return (cache[ key + " " ] = value); } return cache; } @@ -1292,8 +993,21 @@ function createPositionalPseudo( fn ) { } /** - * Detect xml + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist @@ -1302,17 +1016,14 @@ isXML = Sizzle.isXML = function( elem ) { return documentElement ? documentElement.nodeName !== "HTML" : false; }; -// Expose support vars for convenience -support = Sizzle.support = {}; - /** * Sets document-related variables once based on the current document * @param {Element|Object} [doc] An element or document object to use to set the document * @returns {Object} Returns the current document */ setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; // If no document and documentElement is available, return if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { @@ -1322,25 +1033,31 @@ setDocument = Sizzle.setDocument = function( node ) { // Set our document document = doc; docElem = doc.documentElement; - - // Support tests - documentIsHTML = !isXML( doc ); + parent = doc.defaultView; // Support: IE>8 // If iframe document is assigned to "document" variable and if iframe has been reloaded, // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent.attachEvent && parent !== parent.top ) { - parent.attachEvent( "onbeforeunload", function() { - setDocument(); - }); + if ( parent && parent !== parent.top ) { + // IE11 does not have attachEvent, so all must suffer + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } } + /* Support tests + ---------------------------------------------------------------------- */ + documentIsHTML = !isXML( doc ); + /* Attributes ---------------------------------------------------------------------- */ // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) support.attributes = assert(function( div ) { div.className = "i"; return !div.getAttribute("className"); @@ -1355,17 +1072,8 @@ setDocument = Sizzle.setDocument = function( node ) { return !div.getElementsByTagName("*").length; }); - // Check if getElementsByClassName can be trusted - support.getElementsByClassName = assert(function( div ) { - div.innerHTML = "
                  "; - - // Support: Safari<4 - // Catch class over-caching - div.firstChild.className = "i"; - // Support: Opera<10 - // Catch gEBCN failure to find non-leading classes - return div.getElementsByClassName("i").length === 2; - }); + // Support: IE<9 + support.getElementsByClassName = rnative.test( doc.getElementsByClassName ); // Support: IE<10 // Check if getElementById returns elements by name @@ -1379,11 +1087,11 @@ setDocument = Sizzle.setDocument = function( node ) { // ID find and filter if ( support.getById ) { Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var m = context.getElementById( id ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; + return m && m.parentNode ? [ m ] : []; } }; Expr.filter["ID"] = function( id ) { @@ -1400,7 +1108,7 @@ setDocument = Sizzle.setDocument = function( node ) { Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return node && node.value === attrId; }; }; @@ -1409,14 +1117,20 @@ setDocument = Sizzle.setDocument = function( node ) { // Tag Expr.find["TAG"] = support.getElementsByTagName ? function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); } } : + function( tag, context ) { var elem, tmp = [], i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too results = context.getElementsByTagName( tag ); // Filter out possible comments @@ -1434,7 +1148,7 @@ setDocument = Sizzle.setDocument = function( node ) { // Class Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + if ( documentIsHTML ) { return context.getElementsByClassName( className ); } }; @@ -1463,7 +1177,17 @@ setDocument = Sizzle.setDocument = function( node ) { // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; + docElem.appendChild( div ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } // Support: IE8 // Boolean attributes and "value" are not treated correctly @@ -1471,27 +1195,37 @@ setDocument = Sizzle.setDocument = function( node ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } + // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":checked").length ) { rbuggyQSA.push(":checked"); } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } }); assert(function( div ) { - - // Support: Opera 10-12/IE8 - // ^= $= *= and empty values - // Should not select anything // Support: Windows 8 Native Apps - // The type attribute is restricted during .innerHTML assignment + // The type and name attributes are restricted during .innerHTML assignment var input = doc.createElement("input"); input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "t", "" ); + div.appendChild( input ).setAttribute( "name", "D" ); - if ( div.querySelectorAll("[t^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) @@ -1506,7 +1240,8 @@ setDocument = Sizzle.setDocument = function( node ) { }); } - if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector) )) ) { @@ -1528,11 +1263,12 @@ setDocument = Sizzle.setDocument = function( node ) { /* Contains ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another // Purposefully does not implement inclusive descendent // As in, an element does not contain itself - contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? + contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; @@ -1557,7 +1293,7 @@ setDocument = Sizzle.setDocument = function( node ) { ---------------------------------------------------------------------- */ // Document order sorting - sortOrder = docElem.compareDocumentPosition ? + sortOrder = hasCompare ? function( a, b ) { // Flag for duplicate removal @@ -1566,34 +1302,46 @@ setDocument = Sizzle.setDocument = function( node ) { return 0; } - var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); - + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; if ( compare ) { - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + return compare; + } - // Choose the first element that is related to our preferred document - if ( a === doc || contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || contains(preferredDoc, b) ) { - return 1; - } + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; } - return compare & 4 ? -1 : 1; + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; } - // Not directly comparable, sort on existence of method - return a.compareDocumentPosition ? -1 : 1; + return compare & 4 ? -1 : 1; } : function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + var cur, i = 0, aup = a.parentNode, @@ -1601,19 +1349,14 @@ setDocument = Sizzle.setDocument = function( node ) { ap = [ a ], bp = [ b ]; - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { + if ( !aup || !bup ) { return a === doc ? -1 : b === doc ? 1 : aup ? -1 : bup ? 1 : sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : 0; // If the nodes are siblings, we can do a quick check @@ -1676,10 +1419,10 @@ Sizzle.matchesSelector = function( elem, expr ) { elem.document && elem.document.nodeType !== 11 ) { return ret; } - } catch(e) {} + } catch (e) {} } - return Sizzle( expr, document, null, [elem] ).length > 0; + return Sizzle( expr, document, null, [ elem ] ).length > 0; }; Sizzle.contains = function( context, elem ) { @@ -1702,13 +1445,13 @@ Sizzle.attr = function( elem, name ) { fn( elem, name, !documentIsHTML ) : undefined; - return val === undefined ? + return val !== undefined ? + val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : (val = elem.getAttributeNode(name)) && val.specified ? val.value : - null : - val; + null; }; Sizzle.error = function( msg ) { @@ -1741,6 +1484,10 @@ Sizzle.uniqueSort = function( results ) { } } + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + return results; }; @@ -1756,13 +1503,13 @@ getText = Sizzle.getText = function( elem ) { if ( !nodeType ) { // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { + while ( (node = elem[i++]) ) { // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) + // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { @@ -1804,7 +1551,7 @@ Expr = Sizzle.selectors = { match[1] = match[1].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; @@ -1847,15 +1594,15 @@ Expr = Sizzle.selectors = { "PSEUDO": function( match ) { var excess, - unquoted = !match[5] && match[2]; + unquoted = !match[6] && match[2]; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } // Accept quoted arguments as-is - if ( match[3] && match[4] !== undefined ) { - match[2] = match[4]; + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && @@ -1891,7 +1638,7 @@ Expr = Sizzle.selectors = { return pattern || (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); }); }, @@ -1913,7 +1660,7 @@ Expr = Sizzle.selectors = { operator === "^=" ? check && result.indexOf( check ) === 0 : operator === "*=" ? check && result.indexOf( check ) > -1 : operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : false; }; @@ -2033,7 +1780,7 @@ Expr = Sizzle.selectors = { matched = fn( seed, argument ), i = matched.length; while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); + idx = indexOf( seed, matched[i] ); seed[ idx ] = !( matches[ idx ] = matched[i] ); } }) : @@ -2072,6 +1819,8 @@ Expr = Sizzle.selectors = { function( elem, context, xml ) { input[0] = elem; matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; return !results.pop(); }; }), @@ -2083,6 +1832,7 @@ Expr = Sizzle.selectors = { }), "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); return function( elem ) { return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; }; @@ -2159,12 +1909,11 @@ Expr = Sizzle.selectors = { // Contents "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + if ( elem.nodeType < 6 ) { return false; } } @@ -2191,11 +1940,12 @@ Expr = Sizzle.selectors = { "text": function( elem ) { var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); }, // Position-in-collection @@ -2260,7 +2010,7 @@ function setFilters() {} setFilters.prototype = Expr.filters = Expr.pseudos; Expr.setFilters = new setFilters(); -function tokenize( selector, parseOnly ) { +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; @@ -2281,7 +2031,7 @@ function tokenize( selector, parseOnly ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } - groups.push( tokens = [] ); + groups.push( (tokens = []) ); } matched = false; @@ -2325,7 +2075,7 @@ function tokenize( selector, parseOnly ) { Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); -} +}; function toSelector( tokens ) { var i = 0, @@ -2354,8 +2104,8 @@ function addCombinator( matcher, combinator, base ) { // Check against all ancestor/preceding elements function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching if ( xml ) { @@ -2370,14 +2120,17 @@ function addCombinator( matcher, combinator, base ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { return true; } } @@ -2401,6 +2154,15 @@ function elementMatcher( matchers ) { matchers[0]; } +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], @@ -2492,7 +2254,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } @@ -2527,13 +2289,16 @@ function matcherFromTokens( tokens ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; + return indexOf( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; } ]; for ( ; i < len; i++ ) { @@ -2571,31 +2336,30 @@ function matcherFromTokens( tokens ) { } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, + var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { + superMatcher = function( seed, context, xml, results, outermost ) { var elem, j, matcher, - setMatched = [], matchedCount = 0, i = "0", unmatched = seed && [], - outermost = expandContext != null, + setMatched = [], contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; if ( outermost ) { outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; } // Add elements passing elementMatchers directly to results // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; while ( (matcher = elementMatchers[j++]) ) { @@ -2606,7 +2370,6 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { } if ( outermost ) { dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; } } @@ -2671,7 +2434,7 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { superMatcher; } -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], @@ -2679,12 +2442,12 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { if ( !cached ) { // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); + if ( !match ) { + match = tokenize( selector ); } - i = group.length; + i = match.length; while ( i-- ) { - cached = matcherFromTokens( group[i] ); + cached = matcherFromTokens( match[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { @@ -2694,91 +2457,100 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; } return cached; }; -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { var i, tokens, token, type, find, - match = tokenize( selector ); + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { + results = results || []; - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - } - selector = selector.slice( tokens.shift().value.length ); + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; } - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; + selector = selector.slice( tokens.shift().value.length ); + } - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; - break; + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; } + + break; } } } } - // Compile and execute a filtering function + // Compile and execute a filtering function if one is not provided // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( + ( compiled || compile( selector, match ) )( seed, context, !documentIsHTML, results, - rsibling.test( selector ) + rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; -} +}; // One-time assignments // Sort stability support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; -// Support: Chrome<14 +// Support: Chrome 14-35+ // Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = hasDuplicate; +support.detectDuplicates = !!hasDuplicate; // Initialize against the default document setDocument(); @@ -2826,13 +2598,20 @@ if ( !assert(function( div ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { - return (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - elem[ name ] === true ? name.toLowerCase() : null; + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; } }); } +return Sizzle; + +})( window ); + + + jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.pseudos; @@ -2842,66 +2621,473 @@ jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; -})( window ); -// String to Object options format cache -var optionsCache = {}; -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; }); - return object; } -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); + if ( not ) { + expr = ":not(" + expr + ")"; + } - var // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // Flag to know if list is currently firing - firing, - // First callback to fire (used internally by add and fireWith) - firingStart, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + // Inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; + }, + + sibling: function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter(function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.unique( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +}); +var rnotwhite = (/\S+/g); + + + +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks fire = function( data ) { memory = options.memory && data; fired = true; @@ -2966,7 +3152,7 @@ jQuery.Callbacks = function( options ) { if ( list ) { jQuery.each( arguments, function( _, arg ) { var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( firing ) { @@ -3040,6 +3226,8 @@ jQuery.Callbacks = function( options ) { return self; }; + + jQuery.extend({ Deferred: function( func ) { @@ -3062,8 +3250,7 @@ jQuery.extend({ var fns = arguments; return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[1] ](function() { var returned = fn && fn.apply( this, arguments ); @@ -3073,7 +3260,7 @@ jQuery.extend({ .fail( newDefer.reject ) .progress( newDefer.notify ); } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } }); }); @@ -3132,7 +3319,7 @@ jQuery.extend({ // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, - resolveValues = core_slice.call( arguments ), + resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates @@ -3145,8 +3332,8 @@ jQuery.extend({ updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); @@ -3156,7 +3343,7 @@ jQuery.extend({ progressValues, progressContexts, resolveContexts; - // add listeners to Deferred subordinates; treat others as resolved + // Add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); @@ -3173,7 +3360,7 @@ jQuery.extend({ } } - // if we're not waiting on anything, resolve the master + // If we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } @@ -3181,136 +3368,172 @@ jQuery.extend({ return deferred.promise(); } }); -jQuery.support = (function( support ) { - var input = document.createElement("input"), - fragment = document.createDocumentFragment(), - div = document.createElement("div"), - select = document.createElement("select"), - opt = select.appendChild( document.createElement("option") ); - // Finish early in limited environments - if ( !input.type ) { - return support; - } - input.type = "checkbox"; +// The deferred used on DOM ready +var readyList; - // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 - // Check the default checkbox/radio value ("" on old WebKit; "on" elsewhere) - support.checkOn = input.value !== ""; +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); - // Must access the parent to make an option select properly - // Support: IE9, IE10 - support.optSelected = opt.selected; + return this; +}; - // Will be defined later - support.reliableMarginRight = true; - support.boxSizingReliable = true; - support.pixelPosition = false; +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, - // Make sure checked status is properly cloned - // Support: IE9, IE10 - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, - // Check if an input maintains its value after becoming a radio - // Support: IE9, IE10 - input = document.createElement("input"); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; + // Handle when the DOM is ready + ready: function( wait ) { - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } - fragment.appendChild( input ); + // Remember that the DOM is ready + jQuery.isReady = true; - // Support: Safari 5.1, Android 4.x, Android 2.3 - // old WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } - // Support: Firefox, Chrome, Safari - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - support.focusinBubbles = "onfocusin" in window; + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +}); - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, - // Support: Firefox, Android 2.3 (Prefixed box-sizing versions). - divReset = "padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box", - body = document.getElementsByTagName("body")[ 0 ]; +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + jQuery.ready(); +} - if ( !body ) { - // Return for frameset docs that don't have a body - return; +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // We once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); } + } + return readyList.promise( obj ); +}; - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); - // Check box-sizing and margin behavior. - body.appendChild( container ).appendChild( div ); - div.innerHTML = ""; - // Support: Firefox, Android 2.3 (Prefixed box-sizing versions). - div.style.cssText = "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%"; - // Workaround failing boxSizing test due to offsetWidth returning wrong value - // with some non-1 values of body zoom, ticket #13543 - jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { - support.boxSizing = div.offsetWidth === 4; - }); - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - // Support: Android 2.3 - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + if ( fn ) { + for ( ; i < len; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } } + } - body.removeChild( container ); - }); + return chainable ? + elems : - return support; -})( {} ); + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[0], key ) : emptyGet; +}; + + +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( owner ) { + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; -/* - Implementation Summary - - 1. Enforce API surface and semantic compatibility with 1.9.x branch - 2. Improve the module's maintainability by reducing the storage - paths to a single mechanism. - 3. Use the same single mechanism to support "private" and "user" data. - 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) - 5. Avoid exposing implementation details on user objects (eg. expando properties) - 6. Provide a clear path for implementation upgrade to WeakMap in 2014 -*/ -var data_user, data_priv, - rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; function Data() { - // Support: Android < 4, + // Support: Android<4, // Old WebKit does not have Object.preventExtensions/freeze method, // return new empty object instead with no [[set]] accessor Object.defineProperty( this.cache = {}, 0, { @@ -3319,21 +3542,11 @@ function Data() { } }); - this.expando = jQuery.expando + Math.random(); + this.expando = jQuery.expando + Data.uid++; } Data.uid = 1; - -Data.accepts = function( owner ) { - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType ? - owner.nodeType === 1 || owner.nodeType === 9 : true; -}; +Data.accepts = jQuery.acceptData; Data.prototype = { key: function( owner ) { @@ -3357,7 +3570,7 @@ Data.prototype = { descriptor[ this.expando ] = { value: unlock }; Object.defineProperties( owner, descriptor ); - // Support: Android < 4 + // Support: Android<4 // Fallback to a less secure definition } catch ( e ) { descriptor[ this.expando ] = unlock; @@ -3470,7 +3683,7 @@ Data.prototype = { // Otherwise, create an array by matching non-whitespace name = camel; name = name in cache ? - [ name ] : ( name.match( core_rnotwhite ) || [] ); + [ name ] : ( name.match( rnotwhite ) || [] ); } } @@ -3489,5138 +3702,5223 @@ Data.prototype = { if ( owner[ this.expando ] ) { delete this.cache[ owner[ this.expando ] ]; } - } -}; + } +}; +var data_priv = new Data(); + +var data_user = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + data_user.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend({ + hasData: function( elem ) { + return data_user.hasData( elem ) || data_priv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return data_user.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + data_user.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to data_priv methods, these can be deprecated. + _data: function( elem, name, data ) { + return data_priv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + data_priv.remove( elem, name ); + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = data_user.get( elem ); + + if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + data_priv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + data_user.set( this, key ); + }); + } + + return access( this, function( value ) { + var data, + camelKey = jQuery.camelCase( key ); + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + // Attempt to get data from the cache + // with the key as-is + data = data_user.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to get data from the cache + // with the key camelized + data = data_user.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each(function() { + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = data_user.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + data_user.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf("-") !== -1 && data !== undefined ) { + data_user.set( this, key, value ); + } + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + data_user.remove( this, key ); + }); + } +}); + + +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = data_priv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = data_priv.access( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return data_priv.get( elem, key ) || data_priv.access( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + data_priv.remove( elem, [ type + "queue", key ] ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } -// These may be used throughout the jQuery core codebase -data_user = new Data(); -data_priv = new Data(); + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); -jQuery.extend({ - acceptData: Data.accepts, + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); - hasData: function( elem ) { - return data_user.hasData( elem ) || data_priv.hasData( elem ); + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); }, - - data: function( elem, name, data ) { - return data_user.access( elem, name, data ); + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); }, - - removeData: function( elem, name ) { - data_user.remove( elem, name ); + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to data_priv methods, these can be deprecated. - _data: function( elem, name, data ) { - return data_priv.access( elem, name, data ); - }, + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; - _removeData: function( elem, name ) { - data_priv.remove( elem, name ); + while ( i-- ) { + tmp = data_priv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); } }); +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; -jQuery.fn.extend({ - data: function( key, value ) { - var attrs, name, - elem = this[ 0 ], - i = 0, - data = null; +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = data_user.get( elem ); +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + }; - if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[ i ].name; +var rcheckableType = (/^(?:checkbox|radio)$/i); - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - dataAttr( elem, name, data[ name ] ); - } - } - data_priv.set( elem, "hasDataAttrs", true ); - } - } - return data; - } - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - data_user.set( this, key ); - }); - } +(function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); - return jQuery.access( this, function( value ) { - var data, - camelKey = jQuery.camelCase( key ); + // Support: Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - // Attempt to get data from the cache - // with the key as-is - data = data_user.get( elem, key ); - if ( data !== undefined ) { - return data; - } + div.appendChild( input ); - // Attempt to get data from the cache - // with the key camelized - data = data_user.get( elem, camelKey ); - if ( data !== undefined ) { - return data; - } + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, camelKey, undefined ); - if ( data !== undefined ) { - return data; - } + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +})(); +var strundefined = typeof undefined; - // We tried really hard, but the data doesn't exist. - return; - } - // Set the data... - this.each(function() { - // First, attempt to store a copy or reference of any - // data that might've been store with a camelCased key. - var data = data_user.get( this, camelKey ); - // For HTML5 data-* attribute interop, we have to - // store property names with dashes in a camelCase form. - // This might not apply to all properties...* - data_user.set( this, camelKey, value ); +support.focusinBubbles = "onfocusin" in window; - // *... In the case of properties that might _actually_ - // have dashes, we need to also store a copy of that - // unchanged property. - if ( key.indexOf("-") !== -1 && data !== undefined ) { - data_user.set( this, key, value ); - } - }); - }, null, value, arguments.length > 1, null, true ); - }, - removeData: function( key ) { - return this.each(function() { - data_user.remove( this, key ); - }); - } -}); +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; -function dataAttr( elem, key, data ) { - var name; +function returnTrue() { + return true; +} - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - data = elem.getAttribute( name ); +function returnFalse() { + return false; +} - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? JSON.parse( data ) : - data; - } catch( e ) {} +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} - // Make sure we set the data so it isn't changed later - data_user.set( elem, key, data ); - } else { - data = undefined; +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; } - } - return data; -} -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = data_priv.get( elem, type ); + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray( data ) ) { - queue = data_priv.access( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); + // Only use addEventListener if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + } } } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } + if ( special.add ) { + special.add.call( elem, handleObj ); - if ( fn ) { + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); } - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; } - if ( !startLength && hooks ) { - hooks.empty.fire(); - } }, - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return data_priv.get( elem, key ) || data_priv.access( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - data_priv.remove( elem, [ type + "queue", key ] ); - }) - }); - } -}); + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.hasData( elem ) && data_priv.get( elem ); - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; + if ( !elemData || !(events = elemData.events) ) { + return; } - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } } - }; + } - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } - while( i-- ) { - tmp = data_priv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); + delete events[ type ]; } } - resolve(); - return defer.promise( obj ); - } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n\f]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button)$/i; -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + data_priv.remove( elem, "events" ); + } }, - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, + trigger: function( event, data, elem, onlyHandlers ) { - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - removeProp: function( name ) { - return this.each(function() { - delete this[ jQuery.propFix[ name ] || name ]; - }); - }, + cur = tmp = elem = elem || document; - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; } - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); - } - } + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; } - return this; - }, + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); } } } + event.type = type; - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } + // Call a native DOM method on the target with the same name name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - classNames = value.match( core_rnotwhite ) || []; + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); + if ( tmp ) { + elem[ ontype ] = null; } - } - - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - data_priv.set( this, "__className__", this.className ); - } - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : data_priv.get( this, "__className__" ) || ""; - } - }); - }, + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; + if ( tmp ) { + elem[ ontype ] = tmp; + } + } } } - return false; + return event.result; }, - val: function( value ) { - var hooks, ret, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + dispatch: function( event ) { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); - ret = elem.value; + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } - isFunction = jQuery.isFunction( value ); + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - return this.each(function( i ) { - var val; + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; - if ( this.nodeType !== 1 ) { - return; - } + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } + event.handleObj = handleObj; + event.data = handleObj.data; - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } } - }); - } -}); + } -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; + return event.result; + }, - // IE6-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; - // Get the specific value for the option - value = jQuery( option ).val(); + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - // We don't need an array for one selects - if ( one ) { - return value; - } + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; - // Multi-Selects return an array - values.push( value ); + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); } } + } + } - return values; - }, + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; + return handlerQueue; + }, - while ( i-- ) { - option = options[ i ]; - if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { - optionSet = true; - } - } + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - // force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; } + + return event; } }, - attr: function( elem, name, value ) { - var hooks, ret, - nType = elem.nodeType; + mouseHooks: { + props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; } + }, - if ( value !== undefined ) { + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } - if ( value === null ) { - jQuery.removeAttr( elem, name ); + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; - } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - } else { - elem.setAttribute( name, value + "" ); - return value; - } + event = new jQuery.Event( originalEvent ); - } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } - } else { - ret = jQuery.find.attr( elem, name ); + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, - // Boolean attributes get special treatment (#10870) - if ( jQuery.expr.match.bool.test( name ) ) { - // Set corresponding property to false - elem[ propName ] = false; - } + beforeunload: { + postDispatch: function( event ) { - elem.removeAttribute( name ); + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } } } }, - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); } - }, + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } +}; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); - propFix: { - "for": "htmlFor", - "class": "className" - }, + // Mark it as fixed + this[ jQuery.expando ] = true; +}; - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } + preventDefault: function() { + var e = this.originalEvent; - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + this.isDefaultPrevented = returnTrue; - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; + if ( e && e.preventDefault ) { + e.preventDefault(); } + }, + stopPropagation: function() { + var e = this.originalEvent; - if ( value !== undefined ) { - return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? - ret : - ( elem[ name ] = value ); + this.isPropagationStopped = returnTrue; - } else { - return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? - ret : - elem[ name ]; + if ( e && e.stopPropagation ) { + e.stopPropagation(); } }, + stopImmediatePropagation: function() { + var e = this.originalEvent; - propHooks: { - tabIndex: { - get: function( elem ) { - return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ? - elem.tabIndex : - -1; - } - } - } -}); + this.isImmediatePropagationStopped = returnTrue; -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); } - return name; + + this.stopPropagation(); } }; -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; - jQuery.expr.attrHandle[ name ] = function( elem, name, isXML ) { - var fn = jQuery.expr.attrHandle[ name ], - ret = isXML ? - undefined : - /* jshint eqeqeq: false */ - // Temporarily disable this handler to check existence - (jQuery.expr.attrHandle[ name ] = undefined) != - getter( elem, name, isXML ) ? - - name.toLowerCase() : - null; - - // Restore handler - jQuery.expr.attrHandle[ name ] = fn; +// Create mouseenter/leave events using mouseover/out and event-time checks +// Support: Chrome 15+ +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, - return ret; - }; -}); + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; -// Support: IE9+ -// Selectedness for an option in an optgroup can be inaccurate -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; } - return null; + return ret; } }; -} - -jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; }); -// Radios and checkboxes getter/setter -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }; - if ( !jQuery.support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } -}); -var rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; +// Support: Firefox, Chrome, Safari +// Create "bubbling" focus and blur events +if ( !support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { -function returnTrue() { - return true; -} + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; -function returnFalse() { - return false; -} + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ); -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ) - 1; -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + data_priv.remove( doc, fix ); - global: {}, + } else { + data_priv.access( doc, fix, attaches ); + } + } + }; + }); +} - add: function( elem, types, handler, data, selector ) { +jQuery.fn.extend({ - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = data_priv.get( elem ); + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; } - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; } - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; + if ( fn === false ) { + fn = returnFalse; } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, - // Handle multiple events separated by a space - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*\s*$/g, - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); + // We have to close these tags to support XHTML (#13200) + wrapMap = { - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; + // Support: IE9 + option: [ 1, "" ], - // Only use addEventListener if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - } - } - } + thead: [ 1, "", "
                  " ], + col: [ 2, "", "
                  " ], + tr: [ 2, "", "
                  " ], + td: [ 3, "", "
                  " ], - if ( special.add ) { - special.add.call( elem, handleObj ); + _default: [ 0, "", "" ] + }; - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } +// Support: IE9 +wrapMap.optgroup = wrapMap.option; - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } +// Support: 1.x compatibility +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - // Nullify elem to prevent memory leaks in IE - elem = null; - }, + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = data_priv.hasData( elem ) && data_priv.get( elem ); + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute("type"); + } - if ( !elemData || !(events = elemData.events) ) { - return; - } + return elem; +} - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } + for ( ; i < l; i++ ) { + data_priv.set( + elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) + ); + } +} - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; + if ( dest.nodeType !== 1 ) { + return; + } - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); + // 1. Copy private data: events, handlers, etc. + if ( data_priv.hasData( src ) ) { + pdataOld = data_priv.access( src ); + pdataCur = data_priv.set( dest, pdataOld ); + events = pdataOld.events; - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); } - - delete events[ type ]; } } + } - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - data_priv.remove( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { + // 2. Copy user data + if ( data_user.hasData( src ) ) { + udataOld = data_user.access( src ); + udataCur = jQuery.extend( {}, udataOld ); - var i, cur, tmp, bubbleType, ontype, handle, special, - eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + data_user.set( dest, udataCur ); + } +} - cur = tmp = elem = elem || document; +function getAll( context, tag ) { + var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : + context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : + []; - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } } - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); } + } - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); } - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + // Return the cloned set + return clone; + }, - event.type = i > 1 ? - bubbleType : - special.bindType || type; + buildFragment: function( elems, context, scripts, selection ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; - // jQuery handler - handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } + for ( ; i < l; i++ ) { + elem = elems[ i ]; - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; + if ( elem || elem === 0 ) { - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + // Support: QtWebKit, PhantomJS + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); - // Call a native DOM method on the target with the same name name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement("div") ); - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[ 2 ]; - if ( tmp ) { - elem[ ontype ] = null; + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; } - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; + // Support: QtWebKit, PhantomJS + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); - if ( tmp ) { - elem[ ontype ] = tmp; - } + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; } } } - return event.result; - }, - - dispatch: function( event ) { + // Remove wrapper from fragment + fragment.textContent = ""; - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); + i = 0; + while ( (elem = nodes[ i++ ]) ) { - var i, j, ret, matched, handleObj, - handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; + contains = jQuery.contains( elem.ownerDocument, elem ); - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + return fragment; + }, - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + cleanData: function( elems ) { + var data, elem, type, key, + special = jQuery.event.special, + i = 0; - event.handleObj = handleObj; - event.data = handleObj.data; + for ( ; (elem = elems[ i ]) !== undefined; i++ ) { + if ( jQuery.acceptData( elem ) ) { + key = elem[ data_priv.expando ]; - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); + if ( key && (data = data_priv.cache[ key ]) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } } } + if ( data_priv.cache[ key ] ) { + // Discard any remaining `private` data + delete data_priv.cache[ key ]; + } } } + // Discard any remaining `user` data + delete data_user.cache[ elem[ data_user.expando ] ]; } + } +}); - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } +jQuery.fn.extend({ + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each(function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + }); + }, null, value, arguments.length ); + }, - return event.result; + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); }, - handlers: function( event, handlers ) { - var i, matches, sel, handleObj, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, - for ( ; cur !== this; cur = cur.parentNode || this ) { + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.disabled !== true || event.type !== "click" ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; + remove: function( selector, keepData /* Internal Use Only */ ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; + for ( ; (elem = elems[i]) != null; i++ ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); } + elem.parentNode.removeChild( elem ); } } - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; + return this; }, - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + empty: function() { + var elem, + i = 0; - fixHooks: {}, + for ( ; (elem = this[i]) != null; i++ ) { + if ( elem.nodeType === 1 ) { - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; + // Remove any remaining nodes + elem.textContent = ""; } - - return event; } + + return this; }, - mouseHooks: { - props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var eventDoc, doc, body, - button = original.button; + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; + return this.map(function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; } - return event; - } - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + value = value.replace( rxhtmlTag, "<$1>" ); - event = new jQuery.Event( originalEvent ); + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } - // Support: Cordova 2.5 (WebKit) (#13255) - // All events should have a target; Cordova deviceready doesn't - if ( !event.target ) { - event.target = document; - } + elem = 0; - // Support: Safari 6.0+, Chrome < 28 - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } + // If using innerHTML throws an exception, use the fallback method + } catch( e ) {} + } - return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); }, - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - this.focus(); - return false; - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { - this.click(); - return false; - } - }, + replaceWith: function() { + var arg = arguments[ 0 ]; - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + arg = this.parentNode; - beforeunload: { - postDispatch: function( event ) { + jQuery.cleanData( getAll( this ) ); - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined ) { - event.originalEvent.returnValue = event.result; - } + if ( arg ) { + arg.replaceChild( elem, this ); } - } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); }, - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; + detach: function( selector ) { + return this.remove( selector, true ); + }, -jQuery.removeEvent = function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } -}; + domManip: function( args, callback ) { -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } + // Flatten any nested arrays + args = concat.apply( [], args ); - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + self.domManip( args, callback ); + }); + } - // Event type - } else { - this.type = src; - } + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; - // Mark it as fixed - this[ jQuery.expando ] = true; -}; + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); - preventDefault: function() { - var e = this.originalEvent; + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } - this.isDefaultPrevented = returnTrue; + callback.call( this[ i ], node, i ); + } - if ( e && e.preventDefault ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; - this.isPropagationStopped = returnTrue; + // Reenable scripts + jQuery.map( scripts, restoreScript ); - if ( e && e.stopPropagation ) { - e.stopPropagation(); + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } } - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); + + return this; } -}; +}); -// Create mouseenter/leave events using mouseover/out and event-time checks -// Support: Chrome 15+ jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); } + + return this.pushStack( ret ); }; }); -// Create "bubbling" focus and blur events -// Support: Firefox, Chrome, Safari -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; +var iframe, + elemdisplay = {}; - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), -jQuery.fn.extend({ + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var origFn, type; + // Use of this method is a temporary fix (more like optimization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } + return display; +} - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); -var isSimple = /^.[^:#\[\.,]*$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; + if ( !display ) { + display = actualDisplay( nodeName, doc ); -jQuery.fn.extend({ - find: function( selector ) { - var i, - ret = [], - self = this, - len = self.length; + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery( "