1/* ========================================================= 2 * bootstrap-treeview.js v1.3.0 3 * ========================================================= 4 * Copyright 2021 DealiAxy 5 * Project URL : https://github.com/Deali-Axy/bootstrap5-treeview 6 * 7 * (fork from) Copyright 2013 Jonathan Miles 8 * (fork from) Project URL : http://www.jondmiles.com/bootstrap-treeview 9 * 10 * Licensed under the Apache License, Version 2.0 (the "License"); 11 * you may not use this file except in compliance with the License. 12 * You may obtain a copy of the License at 13 * 14 * http://www.apache.org/licenses/LICENSE-2.0 15 * 16 * Unless required by applicable law or agreed to in writing, software 17 * distributed under the License is distributed on an "AS IS" BASIS, 18 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 * See the License for the specific language governing permissions and 20 * limitations under the License. 21 * ========================================================= */ 22 23;(function ($, window, document, undefined) { 24 25 /*global jQuery, console*/ 26 27 'use strict'; 28 29 let pluginName = 'treeview'; 30 31 let _default = {}; 32 33 _default.settings = { 34 35 injectStyle: true, 36 37 levels: 2, 38 39 expandIcon: "fa fa-chevron-right", 40 collapseIcon: "fa fa-chevron-down", 41 emptyIcon: 'fa', 42 nodeIcon: '', 43 selectedIcon: '', 44 checkedIcon: 'fa fa-check-circle-o', 45 uncheckedIcon: 'fa fa-circle-thin', 46 47 color: undefined, // '#000000', 48 backColor: undefined, // '#FFFFFF', 49 borderColor: undefined, // '#dddddd', 50 onhoverColor: '#F5F5F5', 51 selectedColor: '#FFFFFF', 52 selectedBackColor: '#428bca', 53 searchResultColor: '#D9534F', 54 searchResultBackColor: undefined, //'#FFFFFF', 55 56 enableLinks: false, 57 enableIndent: true, // æ·»å äºæ§å¶æ¯å¦å¯ç¨ç¼©è¿çå¼å ³ 58 highlightSelected: true, 59 highlightSearchResults: true, 60 showBorder: true, 61 showIcon: true, 62 showCheckbox: false, 63 showTags: false, 64 multiSelect: false, 65 66 // Event handlers 67 onNodeChecked: undefined, 68 onNodeCollapsed: undefined, 69 onNodeDisabled: undefined, 70 onNodeEnabled: undefined, 71 onNodeExpanded: undefined, 72 onNodeSelected: undefined, 73 onNodeUnchecked: undefined, 74 onNodeUnselected: undefined, 75 onSearchComplete: undefined, 76 onSearchCleared: undefined 77 }; 78 79 _default.options = { 80 silent: false, 81 ignoreChildren: false 82 }; 83 84 _default.searchOptions = { 85 ignoreCase: true, 86 exactMatch: false, 87 revealResults: true 88 }; 89 90 let Tree = function (element, options) { 91 92 this.$element = $(element); 93 this.elementId = element.id; 94 this.styleId = this.elementId + '-style'; 95 96 this.init(options); 97 98 return { 99 100 // Options (public access) 101 options: this.options, 102 103 // Initialize / destroy methods 104 init: $.proxy(this.init, this), 105 remove: $.proxy(this.remove, this), 106 107 // Get methods 108 getNode: $.proxy(this.getNode, this), 109 getParent: $.proxy(this.getParent, this), 110 getSiblings: $.proxy(this.getSiblings, this), 111 getSelected: $.proxy(this.getSelected, this), 112 getUnselected: $.proxy(this.getUnselected, this), 113 getExpanded: $.proxy(this.getExpanded, this), 114 getCollapsed: $.proxy(this.getCollapsed, this), 115 getChecked: $.proxy(this.getChecked, this), 116 getUnchecked: $.proxy(this.getUnchecked, this), 117 getDisabled: $.proxy(this.getDisabled, this), 118 getEnabled: $.proxy(this.getEnabled, this), 119 120 // Select methods 121 selectNode: $.proxy(this.selectNode, this), 122 unselectNode: $.proxy(this.unselectNode, this), 123 toggleNodeSelected: $.proxy(this.toggleNodeSelected, this), 124 125 // Expand / collapse methods 126 collapseAll: $.proxy(this.collapseAll, this), 127 collapseNode: $.proxy(this.collapseNode, this), 128 expandAll: $.proxy(this.expandAll, this), 129 expandNode: $.proxy(this.expandNode, this), 130 toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), 131 revealNode: $.proxy(this.revealNode, this), 132 133 // Expand / collapse methods 134 checkAll: $.proxy(this.checkAll, this), 135 checkNode: $.proxy(this.checkNode, this), 136 uncheckAll: $.proxy(this.uncheckAll, this), 137 uncheckNode: $.proxy(this.uncheckNode, this), 138 toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), 139 140 // Disable / enable methods 141 disableAll: $.proxy(this.disableAll, this), 142 disableNode: $.proxy(this.disableNode, this), 143 enableAll: $.proxy(this.enableAll, this), 144 enableNode: $.proxy(this.enableNode, this), 145 toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), 146 147 // Search methods 148 search: $.proxy(this.search, this), 149 clearSearch: $.proxy(this.clearSearch, this) 150 }; 151 }; 152 153 Tree.prototype.init = function (options) { 154 155 this.tree = []; 156 this.nodes = []; 157 158 if (options.data) { 159 if (typeof options.data === 'string') { 160 options.data = $.parseJSON(options.data); 161 } 162 this.tree = $.extend(true, [], options.data); 163 delete options.data; 164 } 165 this.options = $.extend({}, _default.settings, options); 166 167 this.destroy(); 168 this.subscribeEvents(); 169 this.setInitialStates({nodes: this.tree}, 0); 170 this.render(); 171 }; 172 173 Tree.prototype.remove = function () { 174 this.destroy(); 175 $.removeData(this, pluginName); 176 $('#' + this.styleId).remove(); 177 }; 178 179 Tree.prototype.destroy = function () { 180 181 if (!this.initialized) return; 182 183 this.$wrapper.remove(); 184 this.$wrapper = null; 185 186 // Switch off events 187 this.unsubscribeEvents(); 188 189 // Reset this.initialized flag 190 this.initialized = false; 191 }; 192 193 Tree.prototype.unsubscribeEvents = function () { 194 195 this.$element.off('click'); 196 this.$element.off('nodeChecked'); 197 this.$element.off('nodeCollapsed'); 198 this.$element.off('nodeDisabled'); 199 this.$element.off('nodeEnabled'); 200 this.$element.off('nodeExpanded'); 201 this.$element.off('nodeSelected'); 202 this.$element.off('nodeUnchecked'); 203 this.$element.off('nodeUnselected'); 204 this.$element.off('searchComplete'); 205 this.$element.off('searchCleared'); 206 }; 207 208 Tree.prototype.subscribeEvents = function () { 209 210 this.unsubscribeEvents(); 211 212 this.$element.on('click', $.proxy(this.clickHandler, this)); 213 214 if (typeof (this.options.onNodeChecked) === 'function') { 215 this.$element.on('nodeChecked', this.options.onNodeChecked); 216 } 217 218 if (typeof (this.options.onNodeCollapsed) === 'function') { 219 this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); 220 } 221 222 if (typeof (this.options.onNodeDisabled) === 'function') { 223 this.$element.on('nodeDisabled', this.options.onNodeDisabled); 224 } 225 226 if (typeof (this.options.onNodeEnabled) === 'function') { 227 this.$element.on('nodeEnabled', this.options.onNodeEnabled); 228 } 229 230 if (typeof (this.options.onNodeExpanded) === 'function') { 231 this.$element.on('nodeExpanded', this.options.onNodeExpanded); 232 } 233 234 if (typeof (this.options.onNodeSelected) === 'function') { 235 this.$element.on('nodeSelected', this.options.onNodeSelected); 236 } 237 238 if (typeof (this.options.onNodeUnchecked) === 'function') { 239 this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); 240 } 241 242 if (typeof (this.options.onNodeUnselected) === 'function') { 243 this.$element.on('nodeUnselected', this.options.onNodeUnselected); 244 } 245 246 if (typeof (this.options.onSearchComplete) === 'function') { 247 this.$element.on('searchComplete', this.options.onSearchComplete); 248 } 249 250 if (typeof (this.options.onSearchCleared) === 'function') { 251 this.$element.on('searchCleared', this.options.onSearchCleared); 252 } 253 }; 254 255 /* 256 Recurse the tree structure and ensure all nodes have 257 valid initial states. User defined states will be preserved. 258 For performance we also take this opportunity to 259 index nodes in a flattened structure 260 */ 261 Tree.prototype.setInitialStates = function (node, level) { 262 263 if (!node.nodes) return;
264 level += 1; 265 266 let parent = node; 267 let _this = this; 268 $.each(node.nodes, function checkStates(index, node) { 269 270 // nodeId : unique, incremental identifier 271 node.nodeId = _this.nodes.length; 272 273 // parentId : transversing up the tree 274 node.parentId = parent.nodeId; 275 276 // if not provided set selectable default value 277 if (!node.hasOwnProperty('selectable')) { 278 node.selectable = false; 279 } 280 281 // where provided we should preserve states 282 node.state = node.state || {}; 283 284 // set checked state; unless set always false 285 if (!node.state.hasOwnProperty('checked')) { 286 node.state.checked = false; 287 } 288 289 // set enabled state; unless set always false 290 if (!node.state.hasOwnProperty('disabled')) { 291 node.state.disabled = false; 292 } 293 294 // set expanded state; if not provided based on levels 295 if (!node.state.hasOwnProperty('expanded')) { 296 if (!node.state.disabled && 297 (level < _this.options.levels) && 298 (node.nodes && node.nodes.length > 0)) { 299 node.state.expanded = true; 300 } else { 301 node.state.expanded = false; 302 } 303 } 304 305 // set selected state; unless set always false 306 if (!node.state.hasOwnProperty('selected')) { 307 node.state.selected = false; 308 } 309 310 // index nodes in a flattened structure for use later 311 _this.nodes.push(node); 312 313 // recurse child nodes and transverse the tree 314 if (node.nodes) { 315 _this.setInitialStates(node, level); 316 } 317 }); 318 }; 319 320 Tree.prototype.clickHandler = function (event) { 321 322 if (!this.options.enableLinks) event.preventDefault(); 323 324 let target = $(event.target); 325 let node = this.findNode(target); 326 if (!node || node.state.disabled) return; 327 328 let classList = target.attr('class') ? target.attr('class').split(' ') : []; 329 if ((classList.indexOf('expand-icon') !== -1)) { 330 331 this.toggleExpandedState(node, _default.options); 332 this.render(); 333 } else if ((classList.indexOf('check-icon') !== -1)) { 334 335 this.toggleCheckedState(node, _default.options); 336 this.render(); 337 } else { 338 339 if (node.selectable) { 340 this.toggleSelectedState(node, _default.options); 341 } else { 342 this.toggleExpandedState(node, _default.options); 343 } 344 345 this.render(); 346 } 347 }; 348 349 // Looks up the DOM for the closest parent list item to retrieve the 350 // data attribute nodeid, which is used to lookup the node in the flattened structure. 351 Tree.prototype.findNode = function (target) { 352 353 let nodeId = target.closest('li.list-group-item').attr('data-nodeid'); 354 let node = this.nodes[nodeId]; 355 356 if (!node) { 357 console.log('Error: node does not exist'); 358 } 359 return node; 360 }; 361 362 Tree.prototype.toggleExpandedState = function (node, options) { 363 if (!node) return; 364 this.setExpandedState(node, !node.state.expanded, options); 365 }; 366 367 Tree.prototype.setExpandedState = function (node, state, options) { 368 369 if (state === node.state.expanded) return; 370 371 if (state && node.nodes) { 372 373 // Expand a node 374 node.state.expanded = true; 375 if (!options.silent) { 376 this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); 377 } 378 } else if (!state) { 379 380 // Collapse a node 381 node.state.expanded = false; 382 if (!options.silent) { 383 this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); 384 } 385 386 // Collapse child nodes 387 if (node.nodes && !options.ignoreChildren) { 388 $.each(node.nodes, $.proxy(function (index, node) { 389 this.setExpandedState(node, false, options); 390 }, this)); 391 } 392 } 393 }; 394 395 Tree.prototype.toggleSelectedState = function (node, options) { 396 if (!node) return; 397 this.setSelectedState(node, !node.state.selected, options); 398 }; 399 400 Tree.prototype.setSelectedState = function (node, state, options) { 401 402 if (state === node.state.selected) return; 403 404 if (state) { 405 406 // If multiSelect false, unselect previously selected 407 if (!this.options.multiSelect) { 408 $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { 409 this.setSelectedState(node, false, options); 410 }, this)); 411 } 412 413 // Continue selecting node 414 node.state.selected = true; 415 if (!options.silent) { 416 this.$element.trigger('nodeSelected', $.extend(true, {}, node)); 417 } 418 } else { 419 420 // Unselect node 421 node.state.selected = false; 422 if (!options.silent) { 423 this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); 424 } 425 } 426 }; 427 428 Tree.prototype.toggleCheckedState = function (node, options) { 429 if (!node) return; 430 this.setCheckedState(node, !node.state.checked, options); 431 }; 432 433 Tree.prototype.setCheckedState = function (node, state, options) { 434 435 if (state === node.state.checked) return; 436 437 if (state) { 438 439 // Check node 440 node.state.checked = true; 441 442 if (!options.silent) { 443 this.$element.trigger('nodeChecked', $.extend(true, {}, node)); 444 } 445 } else { 446 447 // Uncheck node 448 node.state.checked = false; 449 if (!options.silent) { 450 this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); 451 } 452 } 453 }; 454 455 Tree.prototype.setDisabledState = function (node, state, options) { 456 457 if (state === node.state.disabled) return; 458 459 if (state) { 460 461 // Disable node 462 node.state.disabled = true; 463 464 // Disable all other states 465 this.setExpandedState(node, false, options); 466 this.setSelectedState(node, false, options); 467 this.setCheckedState(node, false, options); 468 469 if (!options.silent) { 470 this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); 471 } 472 } else { 473 474 // Enabled node 475 node.state.disabled = false; 476 if (!options.silent) { 477 this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); 478 } 479 } 480 }; 481 482 Tree.prototype.render = function () { 483 484 if (!this.initialized) { 485 486 // Setup first time only components 487 this.$element.addClass(pluginName); 488 this.$wrapper = $(this.template.list); 489 490 this.injectStyle(); 491 492 this.initialized = true; 493 } 494 495 this.$element.empty().append(this.$wrapper.empty()); 496 497 // Build tree 498 this.buildTree(this.tree, 0); 499 }; 500 501 // Starting from the root node, and recursing down the 502 // structure we build the tree one node at a time 503 Tree.prototype.buildTree = function (nodes, level) { 504
505 if (!nodes) return; 506 level += 1; 507 508 let _this = this; 509 $.each(nodes, function addNodes(id, node) { 510 511 // æå¤å±å è£ 512 let treeItem = $(_this.template.itemWrapper) 513 .addClass('node-' + _this.elementId) 514 .addClass(node.state.checked ? 'node-checked' : '') 515 .addClass(node.state.disabled ? 'node-disabled' : '') 516 .addClass(node.state.selected ? 'node-selected' : '') 517 .addClass(node.searchResult ? 'search-result' : '') 518 .attr('data-nodeid', node.nodeId) 519 .attr('style', _this.buildStyleOverride(node)); 520 521 // item å åæä¸¤ä¸ªå ç´ ï¼ç¨flexå¸å±åå¸å¨å·¦å³ä¸¤è¾¹ 522 let treeItemLeft = $(_this.template.itemLeftElem) 523 let treeItemRight = $(_this.template.itemRightElem) 524 treeItem.append(treeItemLeft) 525 treeItem.append(treeItemRight) 526 527 // Add indent/spacer to mimic tree structure 528 // æ·»å äºæ§å¶æ¯å¦å¯ç¨ç¼©è¿çå¼å ³ 529 if (_this.options.enableIndent) { 530 for (let i = 0; i < (level - 1); i++) { 531 treeItemLeft.append(_this.template.indent); 532 } 533 } 534 535 // Add expand, collapse or empty spacer icons 536 let classList = []; 537 if (node.nodes) { 538 classList.push('expand-icon'); 539 if (node.state.expanded) { 540 classList.push(_this.options.collapseIcon); 541 } else { 542 classList.push(_this.options.expandIcon); 543 } 544 } else { 545 classList.push(_this.options.emptyIcon); 546 } 547 548 treeItemLeft.append( 549 $(_this.template.icon).addClass(classList.join(' ')) 550 ); 551 552 553 // Add node icon 554 if (_this.options.showIcon) { 555 556 let classList = ['node-icon']; 557 558 classList.push(node.icon || _this.options.nodeIcon); 559 if (node.state.selected) { 560 classList.pop(); 561 classList.push(node.selectedIcon || _this.options.selectedIcon || 562 node.icon || _this.options.nodeIcon); 563 } 564 565 treeItemLeft.append( 566 $(_this.template.icon).addClass(classList.join(' ')) 567 ); 568 } 569 570 // Add check / unchecked icon 571 if (_this.options.showCheckbox) { 572 573 let classList = ['check-icon']; 574 if (node.state.checked) { 575 classList.push(_this.options.checkedIcon); 576 } else { 577 classList.push(_this.options.uncheckedIcon); 578 } 579 580 treeItemLeft.append( 581 $(_this.template.icon).addClass(classList.join(' ')) 582 ); 583 } 584 585 // Add text 586 if (_this.options.enableLinks) { 587 // Add hyperlink 588 treeItemLeft.append( 589 $(_this.template.link) 590 .attr('href', node.href) 591 .append(node.text) 592 ); 593 } else { 594 // otherwise just text 595 treeItemLeft.append(node.text); 596 } 597 598 // Add tags as badges 599 if (_this.options.showTags && node.tags) { 600 $.each(node.tags, function addTag(id, tag) { 601 treeItemRight.append( 602 $(_this.template.badge).append(tag) 603 ); 604 }); 605 } 606 607 // Add item to the tree 608 _this.$wrapper.append(treeItem); 609 610 // Recursively add child ndoes 611 if (node.nodes && node.state.expanded && !node.state.disabled) { 612 return _this.buildTree(node.nodes, level); 613 } 614 }); 615 }; 616 617 // Define any node level style override for 618 // 1. selectedNode 619 // 2. node|data assigned color overrides 620 Tree.prototype.buildStyleOverride = function (node) { 621 622 if (node.state.disabled) return ''; 623 624 let color = node.color; 625 let backColor = node.backColor; 626 627 if (this.options.highlightSelected && node.state.selected) { 628 if (this.options.selectedColor) { 629 color = this.options.selectedColor; 630 } 631 if (this.options.selectedBackColor) { 632 backColor = this.options.selectedBackColor; 633 } 634 } 635 636 if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { 637 if (this.options.searchResultColor) { 638 color = this.options.searchResultColor; 639 } 640 if (this.options.searchResultBackColor) { 641 backColor = this.options.searchResultBackColor; 642 } 643 } 644 645 return 'color:' + color + 646 ';background-color:' + backColor + ';'; 647 }; 648 649 // Add inline style into head 650 Tree.prototype.injectStyle = function () { 651 652 if (this.options.injectStyle && !document.getElementById(this.styleId)) { 653 $('<style type="text/css" id="' + this.styleId + '"> ' + this.buildStyle() + ' </style>').appendTo('head'); 654 } 655 }; 656 657 // Construct trees style based on user options 658 Tree.prototype.buildStyle = function () { 659 660 let style = '.node-' + this.elementId + '{'; 661 662 if (this.options.color) { 663 style += 'color:' + this.options.color + ';'; 664 } 665 666 if (this.options.backColor) { 667 style += 'background-color:' + this.options.backColor + ';'; 668 } 669 670 if (!this.options.showBorder) { 671 style += 'border:none;'; 672 } else if (this.options.borderColor) { 673 style += 'border:1px solid ' + this.options.borderColor + ';'; 674 } 675 style += '}'; 676 677 if (this.options.onhoverColor) { 678 style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + 679 'background-color:' + this.options.onhoverColor + ';' + 680 '}'; 681 } 682 683 return this.css + style; 684 }; 685 686 Tree.prototype.template = { 687 list: '<ul class="list-group"></ul>', 688 itemWrapper: '<li class="list-group-item d-flex justify-content-between align-items-start"></li>', 689 itemLeftElem: '<div class="w-100"></div>', 690 itemRightElem: '<div></div>', 691 indent: '<span class="mx-1"></span>', 692 icon: '<span class="icon"></span>', 693 //link: '<a class="w-75" href="#" style="display:inline-block; color:inherit; text-decoration:none;"></a>', 694 link: '<a class="w-75" style="display:inline-block; color:inherit; text-decoration:none;"></a>', 695 badge: '<span class="ml-1 badge bg-primary rounded-pill"></span>' 696 }; 697 698 Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}' + 699 // '.treeview span.indent{margin-left:5px;margin-right:5px}' + 700 '.treeview span.icon{width:12px;margin-right:5px}' + 701 '.treeview .node-disabled{color:silver;cursor:not-allowed}' 702 703 704 /** 705 Returns a single node object that matches the given node id. 706 @param {Number} nodeId - A node's unique identifier 707 @return {Object} node - Matching node 708 */ 709 Tree.prototype.getNode = function (nodeId) { 710 return this.nodes[nodeId]; 711 }; 712 713 /**
714 Returns the parent node of a given node, if valid otherwise returns undefined. 715 @param {Object|Number} identifier - A valid node or node id 716 @returns {Object} node - The parent node 717 */ 718 Tree.prototype.getParent = function (identifier) { 719 let node = this.identifyNode(identifier); 720 return this.nodes[node.parentId]; 721 }; 722 723 /** 724 Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. 725 @param {Object|Number} identifier - A valid node or node id 726 @returns {Array} nodes - Sibling nodes 727 */ 728 Tree.prototype.getSiblings = function (identifier) { 729 let node = this.identifyNode(identifier); 730 let parent = this.getParent(node); 731 let nodes = parent ? parent.nodes : this.tree; 732 return nodes.filter(function (obj) { 733 return obj.nodeId !== node.nodeId; 734 }); 735 }; 736 737 /** 738 Returns an array of selected nodes. 739 @returns {Array} nodes - Selected nodes 740 */ 741 Tree.prototype.getSelected = function () { 742 return this.findNodes('true', 'g', 'state.selected'); 743 }; 744 745 /** 746 Returns an array of unselected nodes. 747 @returns {Array} nodes - Unselected nodes 748 */ 749 Tree.prototype.getUnselected = function () { 750 return this.findNodes('false', 'g', 'state.selected'); 751 }; 752 753 /** 754 Returns an array of expanded nodes. 755 @returns {Array} nodes - Expanded nodes 756 */ 757 Tree.prototype.getExpanded = function () { 758 return this.findNodes('true', 'g', 'state.expanded'); 759 }; 760 761 /** 762 Returns an array of collapsed nodes. 763 @returns {Array} nodes - Collapsed nodes 764 */ 765 Tree.prototype.getCollapsed = function () { 766 return this.findNodes('false', 'g', 'state.expanded'); 767 }; 768 769 /** 770 Returns an array of checked nodes. 771 @returns {Array} nodes - Checked nodes 772 */ 773 Tree.prototype.getChecked = function () { 774 return this.findNodes('true', 'g', 'state.checked'); 775 }; 776 777 /** 778 Returns an array of unchecked nodes. 779 @returns {Array} nodes - Unchecked nodes 780 */ 781 Tree.prototype.getUnchecked = function () { 782 return this.findNodes('false', 'g', 'state.checked'); 783 }; 784 785 /** 786 Returns an array of disabled nodes. 787 @returns {Array} nodes - Disabled nodes 788 */ 789 Tree.prototype.getDisabled = function () { 790 return this.findNodes('true', 'g', 'state.disabled'); 791 }; 792 793 /** 794 Returns an array of enabled nodes. 795 @returns {Array} nodes - Enabled nodes 796 */ 797 Tree.prototype.getEnabled = function () { 798 return this.findNodes('false', 'g', 'state.disabled'); 799 }; 800 801 802 /** 803 Set a node state to selected 804 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 805 @param {optional Object} options 806 */ 807 Tree.prototype.selectNode = function (identifiers, options) { 808 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 809 this.setSelectedState(node, true, options); 810 }, this)); 811 812 this.render(); 813 }; 814 815 /** 816 Set a node state to unselected 817 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 818 @param {optional Object} options 819 */ 820 Tree.prototype.unselectNode = function (identifiers, options) { 821 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 822 this.setSelectedState(node, false, options); 823 }, this)); 824 825 this.render(); 826 }; 827 828 /** 829 Toggles a node selected state; selecting if unselected, unselecting if selected. 830 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 831 @param {optional Object} options 832 */ 833 Tree.prototype.toggleNodeSelected = function (identifiers, options) { 834 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 835 this.toggleSelectedState(node, options); 836 }, this)); 837 838 this.render(); 839 }; 840 841 842 /** 843 Collapse all tree nodes 844 @param {optional Object} options 845 */ 846 Tree.prototype.collapseAll = function (options) { 847 let identifiers = this.findNodes('true', 'g', 'state.expanded'); 848 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 849 this.setExpandedState(node, false, options); 850 }, this)); 851 852 this.render(); 853 }; 854 855 /** 856 Collapse a given tree node 857 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 858 @param {optional Object} options 859 */ 860 Tree.prototype.collapseNode = function (identifiers, options) { 861 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 862 this.setExpandedState(node, false, options); 863 }, this)); 864 865 this.render(); 866 }; 867 868 /** 869 Expand all tree nodes 870 @param {optional Object} options 871 */ 872 Tree.prototype.expandAll = function (options) { 873 options = $.extend({}, _default.options, options); 874 875 if (options && options.levels) { 876 this.expandLevels(this.tree, options.levels, options); 877 } else { 878 let identifiers = this.findNodes('false', 'g', 'state.expanded'); 879 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 880 this.setExpandedState(node, true, options); 881 }, this)); 882 } 883 884 this.render(); 885 }; 886 887 /** 888 Expand a given tree node 889 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 890 @param {optional Object} options 891 */ 892 Tree.prototype.expandNode = function (identifiers, options) { 893 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 894 this.setExpandedState(node, true, options); 895 if (node.nodes && (options && options.levels)) { 896 this.expandLevels(node.nodes, options.levels - 1, options); 897 } 898 }, this)); 899 900 this.render(); 901 }; 902 903 Tree.prototype.expandLevels = function (nodes, level, options) { 904 options = $.extend({}, _default.options, options); 905 906 $.each(nodes, $.proxy(function (index, node) { 907 this.setExpandedState(node, (level > 0) ? true : false, options); 908 if (node.nodes) { 909 this.expandLevels(node.nodes, level - 1, options); 910 } 911 }, this)); 912 }; 913 914 /** 915 Reveals a given tree node, expanding the tree from node to root. 916 @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers 917 @param {optional Object} options 918 */ 919 Tree.prototype.revealNode = function (identifiers, options) { 920 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 921 let parentNode = this.getParent(node); 922 while (parentNode) { 923 this.setExpandedState(parentNode, true, options); 924 parentNode = this.getParent(parentNode); 925 } 926 ; 927 }, this)); 928 929 this.render(); 930 }; 931 932 /** 933 Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. 934 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 935 @param {optional Object} options 936 */ 937 Tree.prototype.toggleNodeExpanded = function (identifiers, options) { 938 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 939 this.toggleExpandedState(node, options); 940 }, this)); 941 942 this.render(); 943 }; 944 945 946 /** 947 Check all tree nodes 948 @param {optional Object} options 949 */ 950 Tree.prototype.checkAll = function (options) { 951 let identifiers = this.findNodes('false', 'g', 'state.checked'); 952 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 953 this.setCheckedState(node, true, options); 954 }, this)); 955 956 this.render(); 957 }; 958 959 /** 960 Check a given tree node 961 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 962 @param {optional Object} options 963 */ 964 Tree.prototype.checkNode = function (identifiers, options) { 965 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 966 this.setCheckedState(node, true, options); 967 }, this)); 968 969 this.render(); 970 }; 971 972 /** 973 Uncheck all tree nodes 974 @param {optional Object} options 975 */ 976 Tree.prototype.uncheckAll = function (options) { 977 let identifiers = this.findNodes('true', 'g', 'state.checked'); 978 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 979 this.setCheckedState(node, false, options); 980 }, this)); 981 982 this.render(); 983 }; 984 985 /** 986 Uncheck a given tree node 987 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 988 @param {optional Object} options 989 */ 990 Tree.prototype.uncheckNode = function (identifiers, options) { 991 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 992 this.setCheckedState(node, false, options); 993 }, this)); 994 995 this.render(); 996 }; 997 998 /** 999 Toggles a nodes checked state; checking if unchecked, unchecking if checked. 1000 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1001 @param {optional Object} options 1002 */ 1003 Tree.prototype.toggleNodeChecked = function (identifiers, options) { 1004 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1005 this.toggleCheckedState(node, options); 1006 }, this)); 1007 1008 this.render(); 1009 }; 1010 1011 1012 /** 1013 Disable all tree nodes 1014 @param {optional Object} options 1015 */ 1016 Tree.prototype.disableAll = function (options) { 1017 let identifiers = this.findNodes('false', 'g', 'state.disabled'); 1018 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1019 this.setDisabledState(node, true, options); 1020 }, this)); 1021 1022 this.render(); 1023 }; 1024 1025 /** 1026 Disable a given tree node 1027 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1028 @param {optional Object} options 1029 */ 1030 Tree.prototype.disableNode = function (identifiers, options) { 1031 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1032 this.setDisabledState(node, true, options); 1033 }, this)); 1034 1035 this.render(); 1036 }; 1037 1038 /** 1039 Enable all tree nodes 1040 @param {optional Object} options 1041 */ 1042 Tree.prototype.enableAll = function (options) { 1043 let identifiers = this.findNodes('true', 'g', 'state.disabled'); 1044 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1045 this.setDisabledState(node, false, options); 1046 }, this)); 1047 1048 this.render(); 1049 }; 1050 1051 /** 1052 Enable a given tree node 1053 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1054 @param {optional Object} options 1055 */ 1056 Tree.prototype.enableNode = function (identifiers, options) { 1057 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1058 this.setDisabledState(node, false, options); 1059 }, this)); 1060 1061 this.render(); 1062 }; 1063 1064 /** 1065 Toggles a nodes disabled state; disabling is enabled, enabling if disabled. 1066 @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1067 @param {optional Object} options 1068 */ 1069 Tree.prototype.toggleNodeDisabled = function (identifiers, options) { 1070 this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1071 this.setDisabledState(node, !node.state.disabled, options); 1072 }, this)); 1073 1074 this.render(); 1075 }; 1076 1077 1078 /** 1079 Common code for processing multiple identifiers 1080 */ 1081 Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { 1082 1083 options = $.extend({}, _default.options, options); 1084 1085 if (!(identifiers instanceof Array)) { 1086 identifiers = [identifiers]; 1087 } 1088 1089 $.each(identifiers, $.proxy(function (index, identifier) { 1090 callback(this.identifyNode(identifier), options); 1091 }, this)); 1092 }; 1093 1094 /* 1095 Identifies a node from either a node id or object 1096 */ 1097 Tree.prototype.identifyNode = function (identifier) { 1098 return ((typeof identifier) === 'number') ? 1099 this.nodes[identifier] : 1100 identifier; 1101 }; 1102 1103 /** 1104 Searches the tree for nodes (text) that match given criteria 1105 @param {String} pattern - A given string to match against 1106 @param {optional Object} options - Search criteria options 1107 @return {Array} nodes - Matching nodes 1108 */ 1109 Tree.prototype.search = function (pattern, options) { 1110 options = $.extend({}, _default.searchOptions, options); 1111 1112 this.clearSearch({render: false}); 1113 1114 let results = []; 1115 if (pattern && pattern.length > 0) { 1116 1117 if (options.exactMatch) { 1118 pattern = '^' + pattern + '$'; 1119 } 1120 1121 let modifier = 'g'; 1122 if (options.ignoreCase) { 1123 modifier += 'i'; 1124 } 1125 1126 results = this.findNodes(pattern, modifier); 1127 1128 // Add searchResult property to all matching nodes 1129 // This will be used to apply custom styles 1130 // and when identifying result to be cleared 1131 $.each(results, function (index, node) { 1132 node.searchResult = true; 1133 }) 1134 } 1135 1136 // If revealResults, then render is triggered from revealNode 1137 // otherwise we just call render. 1138 if (options.revealResults) { 1139 this.revealNode(results); 1140 } else { 1141 this.render(); 1142 } 1143 1144 this.$element.trigger('searchComplete', $.extend(true, {}, results)); 1145 1146 return results; 1147 }; 1148 1149 /** 1150 Clears previous search results 1151 */ 1152 Tree.prototype.clearSearch = function (options) { 1153 1154 options = $.extend({}, {render: true}, options); 1155 1156 let results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { 1157 node.searchResult = false; 1158 }); 1159 1160 if (options.render) { 1161 this.render(); 1162 } 1163 1164 this.$element.trigger('searchCleared', $.extend(true, {}, results)); 1165 }; 1166 1167 /** 1168 Find nodes that match a given criteria 1169 @param {String} pattern - A given string to match against
1170 @param {optional String} modifier - Valid RegEx modifiers 1171 @param {optional String} attribute - Attribute to compare pattern against 1172 @return {Array} nodes - Nodes that match your criteria 1173 */ 1174 Tree.prototype.findNodes = function (pattern, modifier, attribute) { 1175 1176 modifier = modifier || 'g'; 1177 attribute = attribute || 'text'; 1178 1179 let _this = this; 1180 return $.grep(this.nodes, function (node) { 1181 let val = _this.getNodeValue(node, attribute); 1182 if (typeof val === 'string') { 1183 return val.match(new RegExp(pattern, modifier)); 1184 } 1185 }); 1186 }; 1187 1188 /** 1189 Recursive find for retrieving nested attributes values 1190 All values are return as strings, unless invalid 1191 @param {Object} obj - Typically a node, could be any object 1192 @param {String} attr - Identifies an object property using dot notation 1193 @return {String} value - Matching attributes string representation 1194 */ 1195 Tree.prototype.getNodeValue = function (obj, attr) { 1196 let index = attr.indexOf('.'); 1197 if (index > 0) { 1198 let _obj = obj[attr.substring(0, index)]; 1199 let _attr = attr.substring(index + 1, attr.length); 1200 return this.getNodeValue(_obj, _attr); 1201 } else { 1202 if (obj.hasOwnProperty(attr)) { 1203 return obj[attr].toString(); 1204 } else { 1205 return undefined; 1206 } 1207 } 1208 }; 1209 1210 let logError = function (message) { 1211 if (window.console) { 1212 window.console.error(message); 1213 } 1214 }; 1215 1216 // Prevent against multiple instantiations, 1217 // handle updates and method calls 1218 $.fn[pluginName] = function (options, args) { 1219 1220 let result; 1221 1222 this.each(function () { 1223 let _this = $.data(this, pluginName); 1224 if (typeof options === 'string') { 1225 if (!_this) { 1226 logError('Not initialized, can not call method : ' + options); 1227 } else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') { 1228 logError('No such method : ' + options); 1229 } else { 1230 if (!(args instanceof Array)) { 1231 args = [args]; 1232 } 1233 result = _this[options].apply(_this, args); 1234 } 1235 } else if (typeof options === 'boolean') { 1236 result = _this; 1237 } else { 1238 $.data(this, pluginName, new Tree(this, $.extend(true, {}, options))); 1239 } 1240 }); 1241 1242 return result || this; 1243 }; 1244 1245})(jQuery, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.