/* ========================================================= * bootstrap-treeview.js v1.2.0 * ========================================================= * 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 * * http://www.apache.org/licenses/license-2.0 * * unless required by applicable law or agreed to in writing, software * distributed under the license is distributed on an "as is" basis, * without warranties or conditions of any kind, either express or implied. * see the license for the specific language governing permissions and * limitations under the license. * ========================================================= */ ;(function ($, window, document, undefined) { /*global jquery, console*/ 'use strict'; var pluginname = 'treeview'; var _default = {}; _default.settings = { injectstyle: true, levels: 2, expandicon: 'glyphicon glyphicon-plus', collapseicon: 'glyphicon glyphicon-minus', emptyicon: 'glyphicon', nodeicon: '', selectedicon: '', checkedicon: 'glyphicon glyphicon-check', uncheckedicon: 'glyphicon glyphicon-unchecked', color: undefined, // '#000000', backcolor: undefined, // '#ffffff', bordercolor: undefined, // '#dddddd', onhovercolor: '#f5f5f5', selectedcolor: '#ffffff', selectedbackcolor: '#428bca', searchresultcolor: '#d9534f', searchresultbackcolor: undefined, //'#ffffff', enablelinks: false, highlightselected: true, highlightsearchresults: true, showborder: true, showicon: true, showcheckbox: false, showtags: false, multiselect: false, // event handlers onnodechecked: undefined, onnodecollapsed: undefined, onnodedisabled: undefined, onnodeenabled: undefined, onnodeexpanded: undefined, onnodeselected: undefined, onnodeunchecked: undefined, onnodeunselected: undefined, onsearchcomplete: undefined, onsearchcleared: undefined }; _default.options = { silent: false, ignorechildren: false }; _default.searchoptions = { ignorecase: true, exactmatch: false, revealresults: true }; var tree = function (element, options) { this.$element = $(element); this.elementid = element.id; this.styleid = this.elementid + '-style'; this.init(options); return { // options (public access) options: this.options, // initialize / destroy methods init: $.proxy(this.init, this), remove: $.proxy(this.remove, this), // get methods getnode: $.proxy(this.getnode, this), getparent: $.proxy(this.getparent, this), getsiblings: $.proxy(this.getsiblings, this), getselected: $.proxy(this.getselected, this), getunselected: $.proxy(this.getunselected, this), getexpanded: $.proxy(this.getexpanded, this), getcollapsed: $.proxy(this.getcollapsed, this), getchecked: $.proxy(this.getchecked, this), getunchecked: $.proxy(this.getunchecked, this), getdisabled: $.proxy(this.getdisabled, this), getenabled: $.proxy(this.getenabled, this), // select methods selectnode: $.proxy(this.selectnode, this), unselectnode: $.proxy(this.unselectnode, this), togglenodeselected: $.proxy(this.togglenodeselected, this), // expand / collapse methods collapseall: $.proxy(this.collapseall, this), collapsenode: $.proxy(this.collapsenode, this), expandall: $.proxy(this.expandall, this), expandnode: $.proxy(this.expandnode, this), togglenodeexpanded: $.proxy(this.togglenodeexpanded, this), revealnode: $.proxy(this.revealnode, this), // expand / collapse methods checkall: $.proxy(this.checkall, this), checknode: $.proxy(this.checknode, this), uncheckall: $.proxy(this.uncheckall, this), unchecknode: $.proxy(this.unchecknode, this), togglenodechecked: $.proxy(this.togglenodechecked, this), // disable / enable methods disableall: $.proxy(this.disableall, this), disablenode: $.proxy(this.disablenode, this), enableall: $.proxy(this.enableall, this), enablenode: $.proxy(this.enablenode, this), togglenodedisabled: $.proxy(this.togglenodedisabled, this), // search methods search: $.proxy(this.search, this), clearsearch: $.proxy(this.clearsearch, this) }; }; tree.prototype.init = function (options) { this.tree = []; this.nodes = []; if (options.data) { if (typeof options.data === 'string') { options.data = $.parsejson(options.data); } this.tree = $.extend(true, [], options.data); delete options.data; } this.options = $.extend({}, _default.settings, options); this.destroy(); this.subscribeevents(); this.setinitialstates({ nodes: this.tree }, 0); this.render(); }; tree.prototype.remove = function () { this.destroy(); $.removedata(this, pluginname); $('#' + this.styleid).remove(); }; tree.prototype.destroy = function () { if (!this.initialized) return; this.$wrapper.remove(); this.$wrapper = null; // switch off events this.unsubscribeevents(); // reset this.initialized flag this.initialized = false; }; tree.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'); }; tree.prototype.subscribeevents = function () { this.unsubscribeevents(); 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.onnodecollapsed) === 'function') { this.$element.on('nodecollapsed', this.options.onnodecollapsed); } if (typeof (this.options.onnodedisabled) === 'function') { this.$element.on('nodedisabled', this.options.onnodedisabled); } if (typeof (this.options.onnodeenabled) === 'function') { this.$element.on('nodeenabled', this.options.onnodeenabled); } if (typeof (this.options.onnodeexpanded) === 'function') { this.$element.on('nodeexpanded', this.options.onnodeexpanded); } if (typeof (this.options.onnodeselected) === 'function') { this.$element.on('nodeselected', this.options.onnodeselected); } if (typeof (this.options.onnodeunchecked) === 'function') { this.$element.on('nodeunchecked', this.options.onnodeunchecked); } 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); } }; /* 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 */ tree.prototype.setinitialstates = function (node, level) { if (!node.nodes) return; level += 1; var parent = node; var _this = this; $.each(node.nodes, function checkstates(index, node) { // nodeid : unique, incremental identifier node.nodeid = _this.nodes.length; // parentid : transversing up the tree node.parentid = parent.nodeid; // if not provided set selectable default value if (!node.hasownproperty('selectable')) { node.selectable = true; } // where provided we should preserve states node.state = node.state || {}; // set checked state; unless set always false if (!node.state.hasownproperty('checked')) { node.state.checked = false; } // set enabled state; unless set always false if (!node.state.hasownproperty('disabled')) { node.state.disabled = false; } // set expanded state; if not provided based on levels if (!node.state.hasownproperty('expanded')) { if (!node.state.disabled && (level < _this.options.levels) && (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } else { node.state.expanded = false; } } // set selected state; unless set always false if (!node.state.hasownproperty('selected')) { node.state.selected = false; } // index nodes in a flattened structure for use later _this.nodes.push(node); // recurse child nodes and transverse the tree if (node.nodes) { _this.setinitialstates(node, level); } }); }; tree.prototype.clickhandler = function (event) { if (!this.options.enablelinks) event.preventdefault(); var target = $(event.target); var node = this.findnode(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(); } else if ((classlist.indexof('check-icon') !== -1)) { this.togglecheckedstate(node, _default.options); this.render(); } else { if (node.selectable) { this.toggleselectedstate(node, _default.options); } else { this.toggleexpandedstate(node, _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]; if (!node) { console.log('error: node does not exist'); } return node; }; tree.prototype.toggleexpandedstate = function (node, options) { if (!node) return; this.setexpandedstate(node, !node.state.expanded, options); }; tree.prototype.setexpandedstate = function (node, state, options) { if (state === node.state.expanded) return; if (state && node.nodes) { // expand a node node.state.expanded = true; if (!options.silent) { this.$element.trigger('nodeexpanded', $.extend(true, {}, node)); } } else if (!state) { // collapse a node node.state.expanded = false; if (!options.silent) { this.$element.trigger('nodecollapsed', $.extend(true, {}, node)); } // collapse child nodes if (node.nodes && !options.ignorechildren) { $.each(node.nodes, $.proxy(function (index, node) { this.setexpandedstate(node, false, options); }, this)); } } }; tree.prototype.toggleselectedstate = function (node, options) { if (!node) return; this.setselectedstate(node, !node.state.selected, options); }; tree.prototype.setselectedstate = function (node, state, options) { if (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); }, this)); } // continue selecting node node.state.selected = true; if (!options.silent) { this.$element.trigger('nodeselected', $.extend(true, {}, node)); } } else { // unselect node node.state.selected = false; if (!options.silent) { this.$element.trigger('nodeunselected', $.extend(true, {}, node)); } } }; tree.prototype.togglecheckedstate = function (node, options) { if (!node) return; this.setcheckedstate(node, !node.state.checked, options); }; tree.prototype.setcheckedstate = function (node, state, options) { if (state === node.state.checked) return; if (state) { // check node node.state.checked = true; if (!options.silent) { this.$element.trigger('nodechecked', $.extend(true, {}, node)); } } else { // uncheck node node.state.checked = false; if (!options.silent) { this.$element.trigger('nodeunchecked', $.extend(true, {}, node)); } } }; tree.prototype.setdisabledstate = function (node, state, options) { if (state === node.state.disabled) return; if (state) { // disable node node.state.disabled = true; // disable all other states this.setexpandedstate(node, false, options); this.setselectedstate(node, false, options); this.setcheckedstate(node, false, options); if (!options.silent) { this.$element.trigger('nodedisabled', $.extend(true, {}, node)); } } else { // enabled node node.state.disabled = false; if (!options.silent) { this.$element.trigger('nodeenabled', $.extend(true, {}, node)); } } }; tree.prototype.render = function () { if (!this.initialized) { // setup first time only components this.$element.addclass(pluginname); this.$wrapper = $(this.template.list); this.injectstyle(); this.initialized = true; } this.$element.empty().append(this.$wrapper.empty()); // build tree this.buildtree(this.tree, 0); }; // 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) { if (!nodes) return; level += 1; 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); } // 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); } treeitem .append($(_this.template.icon) .addclass(classlist.join(' ')) ); // add node icon if (_this.options.showicon) { var classlist = ['node-icon']; 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); } treeitem .append($(_this.template.icon) .addclass(classlist.join(' ')) ); } // add check / unchecked icon if (_this.options.showcheckbox) { var classlist = ['check-icon']; if (node.state.checked) { classlist.push(_this.options.checkedicon); } else { classlist.push(_this.options.uncheckedicon); } treeitem .append($(_this.template.icon) .addclass(classlist.join(' ')) ); } // 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 tags as badges if (_this.options.showtags && node.tags) { $.each(node.tags, function addtag(id, tag) { treeitem .append($(_this.template.badge) .append(tag) ); }); } // add item to the tree _this.$wrapper.append(treeitem); // recursively add child ndoes if (node.nodes && node.state.expanded && !node.state.disabled) { return _this.buildtree(node.nodes, level); } }); }; // 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; if (this.options.highlightselected && node.state.selected) { if (this.options.selectedcolor) { color = this.options.selectedcolor; } if (this.options.selectedbackcolor) { backcolor = this.options.selectedbackcolor; } } 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; } } return 'color:' + color + ';background-color:' + backcolor + ';'; }; // add inline style into 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 + '{'; if (this.options.color) { style += 'color:' + this.options.color + ';'; } if (this.options.backcolor) { style += 'background-color:' + this.options.backcolor + ';'; } if (!this.options.showborder) { style += 'border:none;'; } 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 + ';' + '}'; } return this.css + style; }; tree.prototype.template = { list: '', item: '
  • ', 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}' /** returns a single node object that matches the given node id. @param {number} nodeid - a node's unique identifier @return {object} node - matching node */ tree.prototype.getnode = function (nodeid) { return this.nodes[nodeid]; }; /** 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 */ tree.prototype.getparent = function (identifier) { var node = this.identifynode(identifier); return this.nodes[node.parentid]; }; /** 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 */ 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) { return obj.nodeid !== node.nodeid; }); }; /** returns an array of selected nodes. @returns {array} nodes - selected nodes */ tree.prototype.getselected = function () { return this.findnodes('true', 'g', 'state.selected'); }; /** returns an array of unselected nodes. @returns {array} nodes - unselected nodes */ tree.prototype.getunselected = function () { return this.findnodes('false', 'g', 'state.selected'); }; /** returns an array of expanded nodes. @returns {array} nodes - expanded nodes */ tree.prototype.getexpanded = function () { return this.findnodes('true', 'g', 'state.expanded'); }; /** returns an array of collapsed nodes. @returns {array} nodes - collapsed nodes */ tree.prototype.getcollapsed = function () { return this.findnodes('false', 'g', 'state.expanded'); }; /** returns an array of checked nodes. @returns {array} nodes - checked nodes */ tree.prototype.getchecked = function () { return this.findnodes('true', 'g', 'state.checked'); }; /** returns an array of unchecked nodes. @returns {array} nodes - unchecked nodes */ tree.prototype.getunchecked = function () { return this.findnodes('false', 'g', 'state.checked'); }; /** returns an array of disabled nodes. @returns {array} nodes - disabled nodes */ tree.prototype.getdisabled = function () { return this.findnodes('true', 'g', 'state.disabled'); }; /** returns an array of enabled nodes. @returns {array} nodes - enabled nodes */ tree.prototype.getenabled = function () { return this.findnodes('false', 'g', 'state.disabled'); }; /** set a node state to selected @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.selectnode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setselectedstate(node, true, options); }, this)); this.render(); }; /** set a node state to unselected @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.unselectnode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setselectedstate(node, false, options); }, this)); this.render(); }; /** 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 {optional object} options */ tree.prototype.togglenodeselected = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.toggleselectedstate(node, options); }, this)); this.render(); }; /** collapse all tree nodes @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(); }; /** collapse a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.collapsenode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setexpandedstate(node, false, options); }, this)); this.render(); }; /** expand all tree nodes @param {optional object} options */ 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(); }; /** expand a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @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); } }, this)); this.render(); }; tree.prototype.expandlevels = function (nodes, level, options) { options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this.setexpandedstate(node, (level > 0) ? true : false, options); if (node.nodes) { 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 @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); }; }, 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 @param {optional object} options */ tree.prototype.togglenodeexpanded = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.toggleexpandedstate(node, options); }, this)); this.render(); }; /** check all tree nodes @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)); this.render(); }; /** check a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.checknode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setcheckedstate(node, true, options); }, this)); this.render(); }; /** uncheck all tree nodes @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)); this.render(); }; /** uncheck a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.unchecknode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setcheckedstate(node, false, options); }, this)); this.render(); }; /** 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 @param {optional object} options */ tree.prototype.togglenodechecked = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.togglecheckedstate(node, options); }, this)); this.render(); }; /** disable all tree nodes @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)); this.render(); }; /** disable a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.disablenode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setdisabledstate(node, true, options); }, this)); this.render(); }; /** enable all tree nodes @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)); this.render(); }; /** enable a given tree node @param {object|number} identifiers - a valid node, node id or array of node identifiers @param {optional object} options */ tree.prototype.enablenode = function (identifiers, options) { this.foreachidentifier(identifiers, options, $.proxy(function (node, options) { this.setdisabledstate(node, false, options); }, this)); this.render(); }; /** 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); }, this)); this.render(); }; /** common code for processing multiple identifiers */ tree.prototype.foreachidentifier = function (identifiers, options, callback) { options = $.extend({}, _default.options, options); if (!(identifiers instanceof array)) { identifiers = [identifiers]; } $.each(identifiers, $.proxy(function (index, identifier) { callback(this.identifynode(identifier), 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 @param {string} pattern - a given string to match against @param {optional object} options - search criteria options @return {array} nodes - matching nodes */ tree.prototype.search = function (pattern, options) { options = $.extend({}, _default.searchoptions, options); this.clearsearch({ render: false }); var results = []; if (pattern && pattern.length > 0) { if (options.exactmatch) { pattern = '^' + pattern + '$'; } var modifier = 'g'; if (options.ignorecase) { 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; }) } // if revealresults, then render is triggered from revealnode // otherwise we just call render. if (options.revealresults) { this.revealnode(results); } else { this.render(); } this.$element.trigger('searchcomplete', $.extend(true, {}, results)); return results; }; /** 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; }); if (options.render) { this.render(); } this.$element.trigger('searchcleared', $.extend(true, {}, results)); }; /** 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 @return {array} nodes - nodes that match your criteria */ tree.prototype.findnodes = function (pattern, modifier, attribute) { modifier = modifier || 'g'; attribute = attribute || 'text'; var _this = this; return $.grep(this.nodes, function (node) { var val = _this.getnodevalue(node, attribute); if (typeof val === 'string') { return val.match(new regexp(pattern, modifier)); } }); }; /** recursive find for retrieving nested attributes values all values are return as strings, unless invalid @param {object} obj - typically a node, could be any object @param {string} attr - identifies an object property using dot notation @return {string} value - matching attributes string representation */ 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); } else { if (obj.hasownproperty(attr)) { return obj[attr].tostring(); } else { return undefined; } } }; var logerror = function (message) { if (window.console) { window.console.error(message); } }; // prevent against multiple instantiations, // handle updates and method calls $.fn[pluginname] = function (options, args) { var result; this.each(function () { var _this = $.data(this, pluginname); if (typeof options === 'string') { if (!_this) { logerror('not initialized, can not call method : ' + options); } else if (!$.isfunction(_this[options]) || options.charat(0) === '_') { logerror('no such method : ' + options); } else { if (!(args instanceof array)) { args = [ args ]; } result = _this[options].apply(_this, args); } } else if (typeof options === 'boolean') { result = _this; } else { $.data(this, pluginname, new tree(this, $.extend(true, {}, options))); } }); return result || this; }; })(jquery, window, document);