bootstrap-treeview.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951
  1. /* =========================================================
  2. * patternfly-bootstrap-treeview.js v2.1.0
  3. * =========================================================
  4. * Copyright 2013 Jonathan Miles
  5. * Project URL : http://www.jondmiles.com/bootstrap-treeview
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * ========================================================= */
  19. ;(function ($, window, document, undefined) {
  20. /*global jQuery, console*/
  21. 'use strict';
  22. var pluginName = 'treeview';
  23. var _default = {};
  24. _default.settings = {
  25. injectStyle: true,
  26. levels: 2,
  27. expandIcon: 'glyphicon glyphicon-plus',
  28. collapseIcon: 'glyphicon glyphicon-minus',
  29. loadingIcon: 'glyphicon glyphicon-hourglass',
  30. emptyIcon: 'glyphicon',
  31. nodeIcon: '',
  32. selectedIcon: '',
  33. checkedIcon: 'glyphicon glyphicon-check',
  34. partiallyCheckedIcon: 'glyphicon glyphicon-expand',
  35. uncheckedIcon: 'glyphicon glyphicon-unchecked',
  36. color: undefined,
  37. backColor: undefined,
  38. borderColor: undefined,
  39. changedNodeColor: '#39A5DC',
  40. onhoverColor: '#F5F5F5',
  41. selectedColor: '#FFFFFF',
  42. selectedBackColor: '#428bca',
  43. searchResultColor: '#D9534F',
  44. searchResultBackColor: undefined,
  45. highlightSelected: true,
  46. highlightSearchResults: true,
  47. showBorder: true,
  48. showIcon: true,
  49. showImage: false,
  50. showCheckbox: false,
  51. checkboxFirst: false,
  52. highlightChanges: false,
  53. showTags: false,
  54. multiSelect: false,
  55. preventUnselect: false,
  56. allowReselect: false,
  57. hierarchicalCheck: false,
  58. propagateCheckEvent: false,
  59. wrapNodeText: false,
  60. // Event handlers
  61. onLoading: undefined,
  62. onLoadingFailed: undefined,
  63. onInitialized: undefined,
  64. onNodeRendered: undefined,
  65. onRendered: undefined,
  66. onDestroyed: undefined,
  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. _default.options = {
  79. silent: false,
  80. ignoreChildren: false
  81. };
  82. _default.searchOptions = {
  83. ignoreCase: true,
  84. exactMatch: false,
  85. revealResults: true
  86. };
  87. _default.dataUrl = {
  88. method: 'GET',
  89. dataType: 'json',
  90. cache: false
  91. };
  92. var Tree = function (element, options) {
  93. this.$element = $(element);
  94. this._elementId = element.id;
  95. this._styleId = this._elementId + '-style';
  96. this._init(options);
  97. return {
  98. // Options (public access)
  99. options: this._options,
  100. // Initialize / destroy methods
  101. init: $.proxy(this._init, this),
  102. remove: $.proxy(this._remove, this),
  103. // Query methods
  104. findNodes: $.proxy(this.findNodes, this),
  105. getNodes: $.proxy(this.getNodes, this), // todo document + test
  106. getParents: $.proxy(this.getParents, this),
  107. getSiblings: $.proxy(this.getSiblings, this),
  108. getSelected: $.proxy(this.getSelected, this),
  109. getUnselected: $.proxy(this.getUnselected, this),
  110. getExpanded: $.proxy(this.getExpanded, this),
  111. getCollapsed: $.proxy(this.getCollapsed, this),
  112. getChecked: $.proxy(this.getChecked, this),
  113. getUnchecked: $.proxy(this.getUnchecked, this),
  114. getDisabled: $.proxy(this.getDisabled, this),
  115. getEnabled: $.proxy(this.getEnabled, this),
  116. // Tree manipulation methods
  117. addNode: $.proxy(this.addNode, this),
  118. addNodeAfter: $.proxy(this.addNodeAfter, this),
  119. addNodeBefore: $.proxy(this.addNodeBefore, this),
  120. removeNode: $.proxy(this.removeNode, this),
  121. updateNode: $.proxy(this.updateNode, this),
  122. editNode: $.proxy(this.editNode, this),
  123. // Select methods
  124. selectNode: $.proxy(this.selectNode, this),
  125. unselectNode: $.proxy(this.unselectNode, this),
  126. toggleNodeSelected: $.proxy(this.toggleNodeSelected, this),
  127. // Expand / collapse methods
  128. collapseAll: $.proxy(this.collapseAll, this),
  129. collapseNode: $.proxy(this.collapseNode, this),
  130. expandAll: $.proxy(this.expandAll, this),
  131. expandNode: $.proxy(this.expandNode, this),
  132. toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this),
  133. revealNode: $.proxy(this.revealNode, this),
  134. // Check / uncheck methods
  135. checkAll: $.proxy(this.checkAll, this),
  136. checkNode: $.proxy(this.checkNode, this),
  137. uncheckAll: $.proxy(this.uncheckAll, this),
  138. uncheckNode: $.proxy(this.uncheckNode, this),
  139. toggleNodeChecked: $.proxy(this.toggleNodeChecked, this),
  140. unmarkCheckboxChanges: $.proxy(this.unmarkCheckboxChanges, this),
  141. // Disable / enable methods
  142. disableAll: $.proxy(this.disableAll, this),
  143. disableNode: $.proxy(this.disableNode, this),
  144. enableAll: $.proxy(this.enableAll, this),
  145. enableNode: $.proxy(this.enableNode, this),
  146. toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this),
  147. // Search methods
  148. search: $.proxy(this.search, this),
  149. clearSearch: $.proxy(this.clearSearch, this)
  150. };
  151. };
  152. Tree.prototype._init = function (options) {
  153. this._tree = [];
  154. this._initialized = false;
  155. this._options = $.extend({}, _default.settings, options);
  156. // Cache empty icon DOM template
  157. this._template.icon.empty.addClass(this._options.emptyIcon);
  158. this._destroy();
  159. this._subscribeEvents();
  160. this._triggerEvent('loading', null, _default.options);
  161. this._load(options)
  162. .then($.proxy(function (data) {
  163. // load done
  164. return this._tree = $.extend(true, [], data);
  165. }, this), $.proxy(function (error) {
  166. // load fail
  167. this._triggerEvent('loadingFailed', error, _default.options);
  168. }, this))
  169. .then($.proxy(function (treeData) {
  170. // initialize data
  171. return this._setInitialStates({ nodes: treeData }, 0);
  172. }, this))
  173. .then($.proxy(function () {
  174. // render to DOM
  175. this._render();
  176. }, this));
  177. };
  178. Tree.prototype._load = function (options) {
  179. var done = new $.Deferred();
  180. if (options.data) {
  181. this._loadLocalData(options, done);
  182. } else if (options.dataUrl) {
  183. this._loadRemoteData(options, done);
  184. }
  185. return done.promise();
  186. };
  187. Tree.prototype._loadRemoteData = function (options, done) {
  188. $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl))
  189. .done(function (data) {
  190. done.resolve(data);
  191. })
  192. .fail(function (xhr, status, error) {
  193. done.reject(error);
  194. });
  195. };
  196. Tree.prototype._loadLocalData = function (options, done) {
  197. done.resolve((typeof options.data === 'string') ?
  198. JSON.parse(options.data) :
  199. $.extend(true, [], options.data));
  200. };
  201. Tree.prototype._remove = function () {
  202. this._destroy();
  203. $.removeData(this, pluginName);
  204. $('#' + this._styleId).remove();
  205. };
  206. Tree.prototype._destroy = function () {
  207. if (!this._initialized) return;
  208. this._initialized = false;
  209. this._triggerEvent('destroyed', null, _default.options);
  210. // Switch off events
  211. this._unsubscribeEvents();
  212. // Tear down
  213. this.$wrapper.remove();
  214. this.$wrapper = null;
  215. };
  216. Tree.prototype._unsubscribeEvents = function () {
  217. this.$element.off('loading');
  218. this.$element.off('loadingFailed');
  219. this.$element.off('initialized');
  220. this.$element.off('nodeRendered');
  221. this.$element.off('rendered');
  222. this.$element.off('destroyed');
  223. this.$element.off('click');
  224. this.$element.off('nodeChecked');
  225. this.$element.off('nodeCollapsed');
  226. this.$element.off('nodeDisabled');
  227. this.$element.off('nodeEnabled');
  228. this.$element.off('nodeExpanded');
  229. this.$element.off('nodeSelected');
  230. this.$element.off('nodeUnchecked');
  231. this.$element.off('nodeUnselected');
  232. this.$element.off('searchComplete');
  233. this.$element.off('searchCleared');
  234. };
  235. Tree.prototype._subscribeEvents = function () {
  236. this._unsubscribeEvents();
  237. if (typeof (this._options.onLoading) === 'function') {
  238. this.$element.on('loading', this._options.onLoading);
  239. }
  240. if (typeof (this._options.onLoadingFailed) === 'function') {
  241. this.$element.on('loadingFailed', this._options.onLoadingFailed);
  242. }
  243. if (typeof (this._options.onInitialized) === 'function') {
  244. this.$element.on('initialized', this._options.onInitialized);
  245. }
  246. if (typeof (this._options.onNodeRendered) === 'function') {
  247. this.$element.on('nodeRendered', this._options.onNodeRendered);
  248. }
  249. if (typeof (this._options.onRendered) === 'function') {
  250. this.$element.on('rendered', this._options.onRendered);
  251. }
  252. if (typeof (this._options.onDestroyed) === 'function') {
  253. this.$element.on('destroyed', this._options.onDestroyed);
  254. }
  255. this.$element.on('click', $.proxy(this._clickHandler, this));
  256. if (typeof (this._options.onNodeChecked) === 'function') {
  257. this.$element.on('nodeChecked', this._options.onNodeChecked);
  258. }
  259. if (typeof (this._options.onNodeCollapsed) === 'function') {
  260. this.$element.on('nodeCollapsed', this._options.onNodeCollapsed);
  261. }
  262. if (typeof (this._options.onNodeDisabled) === 'function') {
  263. this.$element.on('nodeDisabled', this._options.onNodeDisabled);
  264. }
  265. if (typeof (this._options.onNodeEnabled) === 'function') {
  266. this.$element.on('nodeEnabled', this._options.onNodeEnabled);
  267. }
  268. if (typeof (this._options.onNodeExpanded) === 'function') {
  269. this.$element.on('nodeExpanded', this._options.onNodeExpanded);
  270. }
  271. if (typeof (this._options.onNodeSelected) === 'function') {
  272. this.$element.on('nodeSelected', this._options.onNodeSelected);
  273. }
  274. if (typeof (this._options.onNodeUnchecked) === 'function') {
  275. this.$element.on('nodeUnchecked', this._options.onNodeUnchecked);
  276. }
  277. if (typeof (this._options.onNodeUnselected) === 'function') {
  278. this.$element.on('nodeUnselected', this._options.onNodeUnselected);
  279. }
  280. if (typeof (this._options.onSearchComplete) === 'function') {
  281. this.$element.on('searchComplete', this._options.onSearchComplete);
  282. }
  283. if (typeof (this._options.onSearchCleared) === 'function') {
  284. this.$element.on('searchCleared', this._options.onSearchCleared);
  285. }
  286. };
  287. Tree.prototype._triggerEvent = function (event, data, options) {
  288. if (options && !options.silent) {
  289. this.$element.trigger(event, $.extend(true, {}, data));
  290. }
  291. }
  292. /*
  293. Recurse the tree structure and ensure all nodes have
  294. valid initial states. User defined states will be preserved.
  295. For performance we also take this opportunity to
  296. index nodes in a flattened ordered structure
  297. */
  298. Tree.prototype._setInitialStates = function (node, level) {
  299. this._nodes = {};
  300. return $.when.apply(this, this._setInitialState(node, level))
  301. .done($.proxy(function () {
  302. this._orderedNodes = this._sortNodes();
  303. this._inheritCheckboxChanges();
  304. this._triggerEvent('initialized', this._orderedNodes, _default.options);
  305. return;
  306. }, this));
  307. };
  308. Tree.prototype._setInitialState = function (node, level, done) {
  309. if (!node.nodes) return;
  310. level += 1;
  311. done = done || [];
  312. var parent = node;
  313. $.each(node.nodes, $.proxy(function (index, node) {
  314. var deferred = new $.Deferred();
  315. done.push(deferred.promise());
  316. // level : hierarchical tree level, starts at 1
  317. node.level = level;
  318. // index : relative to siblings
  319. node.index = index;
  320. // nodeId : unique, hierarchical identifier
  321. node.nodeId = (parent && parent.nodeId) ?
  322. parent.nodeId + '.' + node.index :
  323. (level - 1) + '.' + node.index;
  324. // parentId : transversing up the tree
  325. node.parentId = parent.nodeId;
  326. // if not provided set selectable default value
  327. if (!node.hasOwnProperty('selectable')) {
  328. node.selectable = true;
  329. }
  330. // if not provided set checkable default value
  331. if (!node.hasOwnProperty('checkable')) {
  332. node.checkable = true;
  333. }
  334. // where provided we should preserve states
  335. node.state = node.state || {};
  336. // set checked state; unless set always false
  337. if (!node.state.hasOwnProperty('checked')) {
  338. node.state.checked = false;
  339. }
  340. // convert the undefined string if hierarchical checks are enabled
  341. if (this._options.hierarchicalCheck && node.state.checked === 'undefined') {
  342. node.state.checked = undefined;
  343. }
  344. // set enabled state; unless set always false
  345. if (!node.state.hasOwnProperty('disabled')) {
  346. node.state.disabled = false;
  347. }
  348. // set expanded state; if not provided based on levels
  349. if (!node.state.hasOwnProperty('expanded')) {
  350. if (!node.state.disabled &&
  351. (level < this._options.levels) &&
  352. (node.nodes && node.nodes.length > 0)) {
  353. node.state.expanded = true;
  354. }
  355. else {
  356. node.state.expanded = false;
  357. }
  358. }
  359. // set selected state; unless set always false
  360. if (!node.state.hasOwnProperty('selected')) {
  361. node.state.selected = false;
  362. }
  363. // set visible state; based parent state plus levels
  364. if ((parent && parent.state && parent.state.expanded) ||
  365. (level <= this._options.levels)) {
  366. node.state.visible = true;
  367. }
  368. else {
  369. node.state.visible = false;
  370. }
  371. // recurse child nodes and transverse the tree, depth-first
  372. if (node.nodes) {
  373. if (node.nodes.length > 0) {
  374. this._setInitialState(node, level, done);
  375. }
  376. else {
  377. delete node.nodes;
  378. }
  379. }
  380. // add / update indexed collection
  381. this._nodes[node.nodeId] = node;
  382. // mark task as complete
  383. deferred.resolve();
  384. }, this));
  385. return done;
  386. };
  387. Tree.prototype._sortNodes = function () {
  388. return $.map(Object.keys(this._nodes).sort(function (a, b) {
  389. if (a === b) return 0;
  390. var a = a.split('.').map(function (level) { return parseInt(level); });
  391. var b = b.split('.').map(function (level) { return parseInt(level); });
  392. var c = Math.max(a.length, b.length);
  393. for (var i=0; i<c; i++) {
  394. if (a[i] === undefined) return -1;
  395. if (b[i] === undefined) return +1;
  396. if (a[i] - b[i] > 0) return +1;
  397. if (a[i] - b[i] < 0) return -1;
  398. };
  399. }), $.proxy(function (value, index) {
  400. return this._nodes[value];
  401. }, this));
  402. };
  403. Tree.prototype._clickHandler = function (event) {
  404. var target = $(event.target);
  405. var node = this.targetNode(target);
  406. if (!node || node.state.disabled) return;
  407. var classList = target.attr('class') ? target.attr('class').split(' ') : [];
  408. if ((classList.indexOf('expand-icon') !== -1)) {
  409. this._toggleExpanded(node, $.extend({}, _default.options));
  410. }
  411. else if ((classList.indexOf('check-icon') !== -1)) {
  412. if (node.checkable) {
  413. this._toggleChecked(node, $.extend({}, _default.options));
  414. }
  415. }
  416. else {
  417. if (node.selectable) {
  418. this._toggleSelected(node, $.extend({}, _default.options));
  419. } else {
  420. this._toggleExpanded(node, $.extend({}, _default.options));
  421. }
  422. }
  423. };
  424. // Looks up the DOM for the closest parent list item to retrieve the
  425. // data attribute nodeid, which is used to lookup the node in the flattened structure.
  426. Tree.prototype.targetNode = function (target) {
  427. var nodeId = target.closest('li.list-group-item').attr('data-nodeId');
  428. var node = this._nodes[nodeId];
  429. if (!node) {
  430. console.log('Error: node does not exist');
  431. }
  432. return node;
  433. };
  434. Tree.prototype._toggleExpanded = function (node, options) {
  435. if (!node) return;
  436. // Lazy-load the child nodes if possible
  437. if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) {
  438. this._lazyLoad(node);
  439. } else {
  440. this._setExpanded(node, !node.state.expanded, options);
  441. }
  442. };
  443. Tree.prototype._lazyLoad = function (node) {
  444. // Show a different icon while loading the child nodes
  445. node.$el.children('span.expand-icon')
  446. .removeClass(this._options.expandIcon)
  447. .addClass(this._options.loadingIcon);
  448. var _this = this;
  449. this._options.lazyLoad(node, function (nodes) {
  450. // Adding the node will expand its parent automatically
  451. _this.addNode(nodes, node);
  452. });
  453. // Only the first expand should do a lazy-load
  454. delete node.lazyLoad;
  455. };
  456. Tree.prototype._setExpanded = function (node, state, options) {
  457. // We never pass options when rendering, so the only time
  458. // we need to validate state is from user interaction
  459. if (options && state === node.state.expanded) return;
  460. if (state && node.nodes) {
  461. // Set node state
  462. node.state.expanded = true;
  463. // Set element
  464. if (node.$el) {
  465. node.$el.children('span.expand-icon')
  466. .removeClass(this._options.expandIcon)
  467. .removeClass(this._options.loadingIcon)
  468. .addClass(this._options.collapseIcon);
  469. }
  470. // Expand children
  471. if (node.nodes && options) {
  472. $.each(node.nodes, $.proxy(function (index, node) {
  473. this._setVisible(node, true, options);
  474. }, this));
  475. }
  476. // Optionally trigger event
  477. this._triggerEvent('nodeExpanded', node, options);
  478. }
  479. else if (!state) {
  480. // Set node state
  481. node.state.expanded = false;
  482. // Set element
  483. if (node.$el) {
  484. node.$el.children('span.expand-icon')
  485. .removeClass(this._options.collapseIcon)
  486. .addClass(this._options.expandIcon);
  487. }
  488. // Collapse children
  489. if (node.nodes && options) {
  490. $.each(node.nodes, $.proxy(function (index, node) {
  491. this._setVisible(node, false, options);
  492. this._setExpanded(node, false, options);
  493. }, this));
  494. }
  495. // Optionally trigger event
  496. this._triggerEvent('nodeCollapsed', node, options);
  497. }
  498. };
  499. Tree.prototype._setVisible = function (node, state, options) {
  500. if (options && state === node.state.visible) return;
  501. if (state) {
  502. // Set node state
  503. node.state.visible = true;
  504. // Set element
  505. if (node.$el) {
  506. node.$el.removeClass('node-hidden');
  507. }
  508. }
  509. else {
  510. // Set node state to unchecked
  511. node.state.visible = false;
  512. // Set element
  513. if (node.$el) {
  514. node.$el.addClass('node-hidden');
  515. }
  516. }
  517. };
  518. Tree.prototype._toggleSelected = function (node, options) {
  519. if (!node) return;
  520. this._setSelected(node, !node.state.selected, options);
  521. return this;
  522. };
  523. Tree.prototype._setSelected = function (node, state, options) {
  524. // We never pass options when rendering, so the only time
  525. // we need to validate state is from user interaction
  526. if (options && (state === node.state.selected)) return;
  527. if (state) {
  528. // If multiSelect false, unselect previously selected
  529. if (!this._options.multiSelect) {
  530. $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) {
  531. this._setSelected(node, false, $.extend(options, {unselecting: true}));
  532. }, this));
  533. }
  534. // Set node state
  535. node.state.selected = true;
  536. // Set element
  537. if (node.$el) {
  538. node.$el.addClass('node-selected');
  539. if (node.selectedIcon || this._options.selectedIcon) {
  540. node.$el.children('span.node-icon')
  541. .removeClass(node.icon || this._options.nodeIcon)
  542. .addClass(node.selectedIcon || this._options.selectedIcon);
  543. }
  544. }
  545. // Optionally trigger event
  546. this._triggerEvent('nodeSelected', node, options);
  547. }
  548. else {
  549. // If preventUnselect true + only one remaining selection, disable unselect
  550. if (this._options.preventUnselect &&
  551. (options && !options.unselecting) &&
  552. (this._findNodes('true', 'state.selected').length === 1)) {
  553. // Fire the nodeSelected event if reselection is allowed
  554. if (this._options.allowReselect) {
  555. this._triggerEvent('nodeSelected', node, options);
  556. }
  557. return this;
  558. }
  559. // Set node state
  560. node.state.selected = false;
  561. // Set element
  562. if (node.$el) {
  563. node.$el.removeClass('node-selected');
  564. if (node.selectedIcon || this._options.selectedIcon) {
  565. node.$el.children('span.node-icon')
  566. .removeClass(node.selectedIcon || this._options.selectedIcon)
  567. .addClass(node.icon || this._options.nodeIcon);
  568. }
  569. }
  570. // Optionally trigger event
  571. this._triggerEvent('nodeUnselected', node, options);
  572. }
  573. return this;
  574. };
  575. Tree.prototype._inheritCheckboxChanges = function () {
  576. if (this._options.showCheckbox && this._options.highlightChanges) {
  577. this._checkedNodes = $.grep(this._orderedNodes, function (node) {
  578. return node.state.checked;
  579. });
  580. }
  581. };
  582. Tree.prototype._toggleChecked = function (node, options) {
  583. if (!node) return;
  584. if (this._options.hierarchicalCheck) {
  585. // Event propagation to the parent/child nodes
  586. var childOptions = $.extend({}, options, {silent: options.silent || !this._options.propagateCheckEvent});
  587. var state, currentNode = node;
  588. // Temporarily swap the tree state
  589. node.state.checked = !node.state.checked;
  590. // Iterate through each parent node
  591. while (currentNode = this._nodes[currentNode.parentId]) {
  592. // Calculate the state
  593. state = currentNode.nodes.reduce(function (acc, curr) {
  594. return (acc === curr.state.checked) ? acc : undefined;
  595. }, currentNode.nodes[0].state.checked);
  596. // Set the state
  597. this._setChecked(currentNode, state, childOptions);
  598. }
  599. if (node.nodes && node.nodes.length > 0) {
  600. // Copy the content of the array
  601. var child, children = node.nodes.slice();
  602. // Iterate through each child node
  603. while (children && children.length > 0) {
  604. child = children.pop();
  605. // Set the state
  606. this._setChecked(child, node.state.checked, childOptions);
  607. // Append children to the end of the list
  608. if (child.nodes && child.nodes.length > 0) {
  609. children = children.concat(child.nodes.slice());
  610. }
  611. }
  612. }
  613. // Swap back the tree state
  614. node.state.checked = !node.state.checked;
  615. }
  616. this._setChecked(node, !node.state.checked, options);
  617. };
  618. Tree.prototype._setChecked = function (node, state, options) {
  619. // We never pass options when rendering, so the only time
  620. // we need to validate state is from user interaction
  621. if (options && state === node.state.checked) return;
  622. // Highlight the node if its checkbox has unsaved changes
  623. if (this._options.highlightChanges) {
  624. node.$el.toggleClass('node-check-changed', (this._checkedNodes.indexOf(node) == -1) == state);
  625. }
  626. if (state) {
  627. // Set node state
  628. node.state.checked = true;
  629. // Set element
  630. if (node.$el) {
  631. node.$el.addClass('node-checked').removeClass('node-checked-partial');
  632. node.$el.children('span.check-icon')
  633. .removeClass(this._options.uncheckedIcon)
  634. .removeClass(this._options.partiallyCheckedIcon)
  635. .addClass(this._options.checkedIcon);
  636. }
  637. // Optionally trigger event
  638. this._triggerEvent('nodeChecked', node, options);
  639. }
  640. else if (state === undefined && this._options.hierarchicalCheck) {
  641. // Set node state to partially checked
  642. node.state.checked = undefined;
  643. // Set element
  644. if (node.$el) {
  645. node.$el.addClass('node-checked-partial').removeClass('node-checked');
  646. node.$el.children('span.check-icon')
  647. .removeClass(this._options.uncheckedIcon)
  648. .removeClass(this._options.checkedIcon)
  649. .addClass(this._options.partiallyCheckedIcon);
  650. }
  651. // Optionally trigger event, partially checked is technically unchecked
  652. this._triggerEvent('nodeUnchecked', node, options);
  653. } else {
  654. // Set node state to unchecked
  655. node.state.checked = false;
  656. // Set element
  657. if (node.$el) {
  658. node.$el.removeClass('node-checked node-checked-partial');
  659. node.$el.children('span.check-icon')
  660. .removeClass(this._options.checkedIcon)
  661. .removeClass(this._options.partiallyCheckedIcon)
  662. .addClass(this._options.uncheckedIcon);
  663. }
  664. // Optionally trigger event
  665. this._triggerEvent('nodeUnchecked', node, options);
  666. }
  667. };
  668. Tree.prototype._setDisabled = function (node, state, options) {
  669. // We never pass options when rendering, so the only time
  670. // we need to validate state is from user interaction
  671. if (options && state === node.state.disabled) return;
  672. if (state) {
  673. // Set node state to disabled
  674. node.state.disabled = true;
  675. // Disable all other states
  676. if (options && !options.keepState) {
  677. this._setSelected(node, false, options);
  678. this._setChecked(node, false, options);
  679. this._setExpanded(node, false, options);
  680. }
  681. // Set element
  682. if (node.$el) {
  683. node.$el.addClass('node-disabled');
  684. }
  685. // Optionally trigger event
  686. this._triggerEvent('nodeDisabled', node, options);
  687. }
  688. else {
  689. // Set node state to enabled
  690. node.state.disabled = false;
  691. // Set element
  692. if (node.$el) {
  693. node.$el.removeClass('node-disabled');
  694. }
  695. // Optionally trigger event
  696. this._triggerEvent('nodeEnabled', node, options);
  697. }
  698. };
  699. Tree.prototype._setSearchResult = function (node, state, options) {
  700. if (options && state === node.searchResult) return;
  701. if (state) {
  702. node.searchResult = true;
  703. if (node.$el) {
  704. node.$el.addClass('node-result');
  705. }
  706. }
  707. else {
  708. node.searchResult = false;
  709. if (node.$el) {
  710. node.$el.removeClass('node-result');
  711. }
  712. }
  713. };
  714. Tree.prototype._render = function () {
  715. if (!this._initialized) {
  716. // Setup first time only components
  717. this.$wrapper = this._template.tree.clone();
  718. this.$element.empty()
  719. .addClass(pluginName)
  720. .append(this.$wrapper);
  721. this._injectStyle();
  722. this._initialized = true;
  723. }
  724. var previousNode;
  725. $.each(this._orderedNodes, $.proxy(function (id, node) {
  726. this._renderNode(node, previousNode);
  727. previousNode = node;
  728. }, this));
  729. this._triggerEvent('rendered', this._orderedNodes, _default.options);
  730. };
  731. Tree.prototype._renderNode = function (node, previousNode) {
  732. if (!node) return;
  733. if (!node.$el) {
  734. node.$el = this._newNodeEl(node, previousNode)
  735. .addClass('node-' + this._elementId);
  736. }
  737. else {
  738. node.$el.empty();
  739. }
  740. // Append .classes to the node
  741. node.$el.addClass(node.class);
  742. // Set the #id of the node if specified
  743. if (node.id) {
  744. node.$el.attr('id', node.id);
  745. }
  746. // Append custom data- attributes to the node
  747. if (node.dataAttr) {
  748. $.each(node.dataAttr, function (key, value) {
  749. node.$el.attr('data-' + key, value);
  750. });
  751. }
  752. // Set / update nodeid; it can change as a result of addNode etc.
  753. node.$el.attr('data-nodeId', node.nodeId);
  754. // Set the tooltip attribute if present
  755. if (node.tooltip) {
  756. node.$el.attr('title', node.tooltip);
  757. }
  758. // Add indent/spacer to mimic tree structure
  759. for (var i = 0; i < (node.level - 1); i++) {
  760. node.$el.append(this._template.indent.clone());
  761. }
  762. // Add expand / collapse or empty spacer icons
  763. node.$el
  764. .append(
  765. node.nodes || node.lazyLoad ? this._template.icon.expand.clone() : this._template.icon.empty.clone()
  766. );
  767. // Add checkbox and node icons
  768. if (this._options.checkboxFirst) {
  769. this._addCheckbox(node);
  770. this._addIcon(node);
  771. this._addImage(node);
  772. } else {
  773. this._addIcon(node);
  774. this._addImage(node);
  775. this._addCheckbox(node);
  776. }
  777. // Add text
  778. if (this._options.wrapNodeText) {
  779. var wrapper = this._template.text.clone();
  780. node.$el.append(wrapper);
  781. wrapper.append(node.text);
  782. } else {
  783. node.$el.append(node.text);
  784. }
  785. // Add tags as badges
  786. if (this._options.showTags && node.tags) {
  787. $.each(node.tags, $.proxy(function addTag(id, tag) {
  788. node.$el
  789. .append(this._template.badge.clone()
  790. .append(tag)
  791. );
  792. }, this));
  793. }
  794. // Set various node states
  795. this._setSelected(node, node.state.selected);
  796. this._setChecked(node, node.state.checked);
  797. this._setSearchResult(node, node.searchResult);
  798. this._setExpanded(node, node.state.expanded);
  799. this._setDisabled(node, node.state.disabled);
  800. this._setVisible(node, node.state.visible);
  801. // Trigger nodeRendered event
  802. this._triggerEvent('nodeRendered', node, _default.options);
  803. };
  804. // Add checkable icon
  805. Tree.prototype._addCheckbox = function (node) {
  806. if (this._options.showCheckbox && (node.hideCheckbox === undefined || node.hideCheckbox === false)) {
  807. node.$el
  808. .append(this._template.icon.check.clone());
  809. }
  810. }
  811. // Add node icon
  812. Tree.prototype._addIcon = function (node) {
  813. if (this._options.showIcon && !(this._options.showImage && node.image)) {
  814. node.$el
  815. .append(this._template.icon.node.clone()
  816. .addClass(node.icon || this._options.nodeIcon)
  817. );
  818. }
  819. }
  820. Tree.prototype._addImage = function (node) {
  821. if (this._options.showImage && node.image) {
  822. node.$el
  823. .append(this._template.image.clone()
  824. .addClass('node-image')
  825. .css('background-image', "url('" + node.image + "')")
  826. );
  827. }
  828. }
  829. // Creates a new node element from template and
  830. // ensures the template is inserted at the correct position
  831. Tree.prototype._newNodeEl = function (node, previousNode) {
  832. var $el = this._template.node.clone();
  833. if (previousNode) {
  834. // typical usage, as nodes are rendered in
  835. // sort order we add after the previous element
  836. previousNode.$el.after($el);
  837. } else {
  838. // we use prepend instead of append,
  839. // to cater for root inserts i.e. nodeId 0.0
  840. this.$wrapper.prepend($el);
  841. }
  842. return $el;
  843. };
  844. // Recursively remove node elements from DOM
  845. Tree.prototype._removeNodeEl = function (node) {
  846. if (!node) return;
  847. if (node.nodes) {
  848. $.each(node.nodes, $.proxy(function (index, node) {
  849. this._removeNodeEl(node);
  850. }, this));
  851. }
  852. node.$el.remove();
  853. };
  854. // Expand node, rendering it's immediate children
  855. Tree.prototype._expandNode = function (node) {
  856. if (!node.nodes) return;
  857. $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) {
  858. childNode.level = node.level + 1;
  859. this._renderNode(childNode, node.$el);
  860. }, this));
  861. };
  862. // Add inline style into head
  863. Tree.prototype._injectStyle = function () {
  864. if (this._options.injectStyle && !document.getElementById(this._styleId)) {
  865. $('<style type="text/css" id="' + this._styleId + '"> ' + this._buildStyle() + ' </style>').appendTo('head');
  866. }
  867. };
  868. // Construct trees style based on user options
  869. Tree.prototype._buildStyle = function () {
  870. var style = '.node-' + this._elementId + '{';
  871. // Basic bootstrap style overrides
  872. if (this._options.color) {
  873. style += 'color:' + this._options.color + ';';
  874. }
  875. if (this._options.backColor) {
  876. style += 'background-color:' + this._options.backColor + ';';
  877. }
  878. if (!this._options.showBorder) {
  879. style += 'border:none;';
  880. }
  881. else if (this._options.borderColor) {
  882. style += 'border:1px solid ' + this._options.borderColor + ';';
  883. }
  884. style += '}';
  885. if (this._options.onhoverColor) {
  886. style += '.node-' + this._elementId + ':not(.node-disabled):hover{' +
  887. 'background-color:' + this._options.onhoverColor + ';' +
  888. '}';
  889. }
  890. // Style search results
  891. if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) {
  892. var innerStyle = ''
  893. if (this._options.searchResultColor) {
  894. innerStyle += 'color:' + this._options.searchResultColor + ';';
  895. }
  896. if (this._options.searchResultBackColor) {
  897. innerStyle += 'background-color:' + this._options.searchResultBackColor + ';';
  898. }
  899. style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}';
  900. style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}';
  901. }
  902. // Style selected nodes
  903. if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) {
  904. var innerStyle = ''
  905. if (this._options.selectedColor) {
  906. innerStyle += 'color:' + this._options.selectedColor + ';';
  907. }
  908. if (this._options.selectedBackColor) {
  909. innerStyle += 'background-color:' + this._options.selectedBackColor + ';';
  910. }
  911. style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}';
  912. style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}';
  913. }
  914. // Style changed nodes
  915. if (this._options.highlightChanges) {
  916. var innerStyle = 'color: ' + this._options.changedNodeColor + ';';
  917. style += '.node-' + this._elementId + '.node-check-changed{' + innerStyle + '}';
  918. }
  919. // Node level style overrides
  920. $.each(this._orderedNodes, $.proxy(function (index, node) {
  921. if (node.color || node.backColor) {
  922. var innerStyle = '';
  923. if (node.color) {
  924. innerStyle += 'color:' + node.color + ';';
  925. }
  926. if (node.backColor) {
  927. innerStyle += 'background-color:' + node.backColor + ';';
  928. }
  929. style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}';
  930. }
  931. }, this));
  932. return this._css + style;
  933. };
  934. Tree.prototype._template = {
  935. tree: $('<ul class="list-group"></ul>'),
  936. node: $('<li class="list-group-item"></li>'),
  937. indent: $('<span class="indent"></span>'),
  938. icon: {
  939. node: $('<span class="icon node-icon"></span>'),
  940. expand: $('<span class="icon expand-icon"></span>'),
  941. check: $('<span class="icon check-icon"></span>'),
  942. empty: $('<span class="icon"></span>')
  943. },
  944. image: $('<span class="image"></span>'),
  945. badge: $('<span class="badge"></span>'),
  946. text: $('<span class="text"></span>')
  947. };
  948. 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}'
  949. /**
  950. Returns an array of matching node objects.
  951. @param {String} pattern - A pattern to match against a given field
  952. @return {String} field - Field to query pattern against
  953. */
  954. Tree.prototype.findNodes = function (pattern, field) {
  955. return this._findNodes(pattern, field);
  956. };
  957. /**
  958. Returns an ordered aarray of node objects.
  959. @return {Array} nodes - An array of all nodes
  960. */
  961. Tree.prototype.getNodes = function () {
  962. return this._orderedNodes;
  963. };
  964. /**
  965. Returns parent nodes for given nodes, if valid otherwise returns undefined.
  966. @param {Array} nodes - An array of nodes
  967. @returns {Array} nodes - An array of parent nodes
  968. */
  969. Tree.prototype.getParents = function (nodes) {
  970. if (!(nodes instanceof Array)) {
  971. nodes = [nodes];
  972. }
  973. var parentNodes = [];
  974. $.each(nodes, $.proxy(function (index, node) {
  975. var parentNode = node.parentId ? this._nodes[node.parentId] : false;
  976. if (parentNode) {
  977. parentNodes.push(parentNode);
  978. }
  979. }, this));
  980. return parentNodes;
  981. };
  982. /**
  983. Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined.
  984. @param {Array} nodes - An array of nodes
  985. @returns {Array} nodes - An array of sibling nodes
  986. */
  987. Tree.prototype.getSiblings = function (nodes) {
  988. if (!(nodes instanceof Array)) {
  989. nodes = [nodes];
  990. }
  991. var siblingNodes = [];
  992. $.each(nodes, $.proxy(function (index, node) {
  993. var parent = this.getParents([node]);
  994. var nodes = parent[0] ? parent[0].nodes : this._tree;
  995. siblingNodes = nodes.filter(function (obj) {
  996. return obj.nodeId !== node.nodeId;
  997. });
  998. }, this));
  999. // flatten possible nested array before returning
  1000. return $.map(siblingNodes, function (obj) {
  1001. return obj;
  1002. });
  1003. };
  1004. /**
  1005. Returns an array of selected nodes.
  1006. @returns {Array} nodes - Selected nodes
  1007. */
  1008. Tree.prototype.getSelected = function () {
  1009. return this._findNodes('true', 'state.selected');
  1010. };
  1011. /**
  1012. Returns an array of unselected nodes.
  1013. @returns {Array} nodes - Unselected nodes
  1014. */
  1015. Tree.prototype.getUnselected = function () {
  1016. return this._findNodes('false', 'state.selected');
  1017. };
  1018. /**
  1019. Returns an array of expanded nodes.
  1020. @returns {Array} nodes - Expanded nodes
  1021. */
  1022. Tree.prototype.getExpanded = function () {
  1023. return this._findNodes('true', 'state.expanded');
  1024. };
  1025. /**
  1026. Returns an array of collapsed nodes.
  1027. @returns {Array} nodes - Collapsed nodes
  1028. */
  1029. Tree.prototype.getCollapsed = function () {
  1030. return this._findNodes('false', 'state.expanded');
  1031. };
  1032. /**
  1033. Returns an array of checked nodes.
  1034. @returns {Array} nodes - Checked nodes
  1035. */
  1036. Tree.prototype.getChecked = function () {
  1037. return this._findNodes('true', 'state.checked');
  1038. };
  1039. /**
  1040. Returns an array of unchecked nodes.
  1041. @returns {Array} nodes - Unchecked nodes
  1042. */
  1043. Tree.prototype.getUnchecked = function () {
  1044. return this._findNodes('false', 'state.checked');
  1045. };
  1046. /**
  1047. Returns an array of disabled nodes.
  1048. @returns {Array} nodes - Disabled nodes
  1049. */
  1050. Tree.prototype.getDisabled = function () {
  1051. return this._findNodes('true', 'state.disabled');
  1052. };
  1053. /**
  1054. Returns an array of enabled nodes.
  1055. @returns {Array} nodes - Enabled nodes
  1056. */
  1057. Tree.prototype.getEnabled = function () {
  1058. return this._findNodes('false', 'state.disabled');
  1059. };
  1060. /**
  1061. Add nodes to the tree.
  1062. @param {Array} nodes - An array of nodes to add
  1063. @param {optional Object} parentNode - The node to which nodes will be added as children
  1064. @param {optional number} index - Zero based insert index
  1065. @param {optional Object} options
  1066. */
  1067. Tree.prototype.addNode = function (nodes, parentNode, index, options) {
  1068. if (!(nodes instanceof Array)) {
  1069. nodes = [nodes];
  1070. }
  1071. if (parentNode instanceof Array) {
  1072. parentNode = parentNode[0];
  1073. }
  1074. options = $.extend({}, _default.options, options);
  1075. // identify target nodes; either the tree's root or a parent's child nodes
  1076. var targetNodes;
  1077. if (parentNode && parentNode.nodes) {
  1078. targetNodes = parentNode.nodes;
  1079. } else if (parentNode) {
  1080. targetNodes = parentNode.nodes = [];
  1081. } else {
  1082. targetNodes = this._tree;
  1083. }
  1084. // inserting nodes at specified positions
  1085. $.each(nodes, $.proxy(function (i, node) {
  1086. var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1);
  1087. targetNodes.splice(insertIndex, 0, node);
  1088. }, this));
  1089. // initialize new state and render changes
  1090. this._setInitialStates({nodes: this._tree}, 0)
  1091. .done($.proxy(function () {
  1092. if (parentNode && !parentNode.state.expanded) {
  1093. this._setExpanded(parentNode, true, options);
  1094. }
  1095. this._render();
  1096. }, this));
  1097. }
  1098. /**
  1099. Add nodes to the tree after given node.
  1100. @param {Array} nodes - An array of nodes to add
  1101. @param {Object} node - The node to which nodes will be added after
  1102. @param {optional Object} options
  1103. */
  1104. Tree.prototype.addNodeAfter = function (nodes, node, options) {
  1105. if (!(nodes instanceof Array)) {
  1106. nodes = [nodes];
  1107. }
  1108. if (node instanceof Array) {
  1109. node = node[0];
  1110. }
  1111. options = $.extend({}, _default.options, options);
  1112. this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options);
  1113. }
  1114. /**
  1115. Add nodes to the tree before given node.
  1116. @param {Array} nodes - An array of nodes to add
  1117. @param {Object} node - The node to which nodes will be added before
  1118. @param {optional Object} options
  1119. */
  1120. Tree.prototype.addNodeBefore = function (nodes, node, options) {
  1121. if (!(nodes instanceof Array)) {
  1122. nodes = [nodes];
  1123. }
  1124. if (node instanceof Array) {
  1125. node = node[0];
  1126. }
  1127. options = $.extend({}, _default.options, options);
  1128. this.addNode(nodes, this.getParents(node)[0], node.index, options);
  1129. }
  1130. /**
  1131. Removes given nodes from the tree.
  1132. @param {Array} nodes - An array of nodes to remove
  1133. @param {optional Object} options
  1134. */
  1135. Tree.prototype.removeNode = function (nodes, options) {
  1136. if (!(nodes instanceof Array)) {
  1137. nodes = [nodes];
  1138. }
  1139. options = $.extend({}, _default.options, options);
  1140. var targetNodes, parentNode;
  1141. $.each(nodes, $.proxy(function (index, node) {
  1142. // remove nodes from tree
  1143. parentNode = this._nodes[node.parentId];
  1144. if (parentNode) {
  1145. targetNodes = parentNode.nodes;
  1146. } else {
  1147. targetNodes = this._tree;
  1148. }
  1149. targetNodes.splice(node.index, 1);
  1150. // remove node from DOM
  1151. this._removeNodeEl(node);
  1152. }, this));
  1153. // initialize new state and render changes
  1154. this._setInitialStates({nodes: this._tree}, 0)
  1155. .done(this._render.bind(this));
  1156. };
  1157. /**
  1158. Updates / replaces a given tree node
  1159. @param {Object} node - A single node to be replaced
  1160. @param {Object} newNode - THe replacement node
  1161. @param {optional Object} options
  1162. */
  1163. Tree.prototype.updateNode = function (node, newNode, options) {
  1164. if (node instanceof Array) {
  1165. node = node[0];
  1166. }
  1167. options = $.extend({}, _default.options, options);
  1168. // insert new node
  1169. $.extend(node,newNode);
  1170. // remove old node from DOM
  1171. //this._removeNodeEl(node);
  1172. // initialize new state and render changes
  1173. this._setInitialStates({nodes: this._tree}, 0)
  1174. .done(this._render.bind(this));
  1175. };
  1176. /**
  1177. Selects given tree nodes
  1178. @param {Array} nodes - An array of nodes
  1179. @param {optional Object} options
  1180. */
  1181. Tree.prototype.selectNode = function (nodes, options) {
  1182. if (!(nodes instanceof Array)) {
  1183. nodes = [nodes];
  1184. }
  1185. options = $.extend({}, _default.options, options);
  1186. $.each(nodes, $.proxy(function (index, node) {
  1187. this._setSelected(node, true, options);
  1188. }, this));
  1189. };
  1190. /**
  1191. Unselects given tree nodes
  1192. @param {Array} nodes - An array of nodes
  1193. @param {optional Object} options
  1194. */
  1195. Tree.prototype.unselectNode = function (nodes, options) {
  1196. if (!(nodes instanceof Array)) {
  1197. nodes = [nodes];
  1198. }
  1199. options = $.extend({}, _default.options, options);
  1200. $.each(nodes, $.proxy(function (index, node) {
  1201. this._setSelected(node, false, options);
  1202. }, this));
  1203. };
  1204. /**
  1205. Toggles a node selected state; selecting if unselected, unselecting if selected.
  1206. @param {Array} nodes - An array of nodes
  1207. @param {optional Object} options
  1208. */
  1209. Tree.prototype.toggleNodeSelected = function (nodes, options) {
  1210. if (!(nodes instanceof Array)) {
  1211. nodes = [nodes];
  1212. }
  1213. options = $.extend({}, _default.options, options);
  1214. $.each(nodes, $.proxy(function (index, node) {
  1215. this._toggleSelected(node, options);
  1216. }, this));
  1217. };
  1218. /**
  1219. Collapse all tree nodes
  1220. @param {optional Object} options
  1221. */
  1222. Tree.prototype.collapseAll = function (options) {
  1223. options = $.extend({}, _default.options, options);
  1224. options.levels = options.levels || 999;
  1225. this.collapseNode(this._tree, options);
  1226. };
  1227. /**
  1228. Collapse a given tree node
  1229. @param {Array} nodes - An array of nodes
  1230. @param {optional Object} options
  1231. */
  1232. Tree.prototype.collapseNode = function (nodes, options) {
  1233. options = $.extend({}, _default.options, options);
  1234. $.each(nodes, $.proxy(function (index, node) {
  1235. this._setExpanded(node, false, options);
  1236. }, this));
  1237. };
  1238. /**
  1239. Expand all tree nodes
  1240. @param {optional Object} options
  1241. */
  1242. Tree.prototype.expandAll = function (options) {
  1243. options = $.extend({}, _default.options, options);
  1244. options.levels = options.levels || 999;
  1245. this.expandNode(this._tree, options);
  1246. };
  1247. /**
  1248. Expand given tree nodes
  1249. @param {Array} nodes - An array of nodes
  1250. @param {optional Object} options
  1251. */
  1252. Tree.prototype.expandNode = function (nodes, options) {
  1253. if (!(nodes instanceof Array)) {
  1254. nodes = [nodes];
  1255. }
  1256. options = $.extend({}, _default.options, options);
  1257. $.each(nodes, $.proxy(function (index, node) {
  1258. // Do not re-expand already expanded nodes
  1259. if (node.state.expanded) return;
  1260. if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) {
  1261. this._lazyLoad(node);
  1262. }
  1263. this._setExpanded(node, true, options);
  1264. if (node.nodes) {
  1265. this._expandLevels(node.nodes, options.levels-1, options);
  1266. }
  1267. }, this));
  1268. };
  1269. Tree.prototype._expandLevels = function (nodes, level, options) {
  1270. if (!(nodes instanceof Array)) {
  1271. nodes = [nodes];
  1272. }
  1273. options = $.extend({}, _default.options, options);
  1274. $.each(nodes, $.proxy(function (index, node) {
  1275. this._setExpanded(node, (level > 0) ? true : false, options);
  1276. if (node.nodes) {
  1277. this._expandLevels(node.nodes, level-1, options);
  1278. }
  1279. }, this));
  1280. };
  1281. /**
  1282. Reveals given tree nodes, expanding the tree from node to root.
  1283. @param {Array} nodes - An array of nodes
  1284. @param {optional Object} options
  1285. */
  1286. Tree.prototype.revealNode = function (nodes, options) {
  1287. if (!(nodes instanceof Array)) {
  1288. nodes = [nodes];
  1289. }
  1290. options = $.extend({}, _default.options, options);
  1291. $.each(nodes, $.proxy(function (index, node) {
  1292. var parentNode = node;
  1293. var tmpNode;
  1294. while (tmpNode = this.getParents([parentNode])[0]) {
  1295. parentNode = tmpNode;
  1296. this._setExpanded(parentNode, true, options);
  1297. };
  1298. }, this));
  1299. };
  1300. /**
  1301. Toggles a node's expanded state; collapsing if expanded, expanding if collapsed.
  1302. @param {Array} nodes - An array of nodes
  1303. @param {optional Object} options
  1304. */
  1305. Tree.prototype.toggleNodeExpanded = function (nodes, options) {
  1306. if (!(nodes instanceof Array)) {
  1307. nodes = [nodes];
  1308. }
  1309. options = $.extend({}, _default.options, options);
  1310. $.each(nodes, $.proxy(function (index, node) {
  1311. this._toggleExpanded(node, options);
  1312. }, this));
  1313. };
  1314. /**
  1315. Check all tree nodes
  1316. @param {optional Object} options
  1317. */
  1318. Tree.prototype.checkAll = function (options) {
  1319. options = $.extend({}, _default.options, options);
  1320. var nodes = $.grep(this._orderedNodes, function (node) {
  1321. return !node.state.checked;
  1322. });
  1323. $.each(nodes, $.proxy(function (index, node) {
  1324. this._setChecked(node, true, options);
  1325. }, this));
  1326. };
  1327. /**
  1328. Checks given tree nodes
  1329. @param {Array} nodes - An array of nodes
  1330. @param {optional Object} options
  1331. */
  1332. Tree.prototype.checkNode = function (nodes, options) {
  1333. if (!(nodes instanceof Array)) {
  1334. nodes = [nodes];
  1335. }
  1336. options = $.extend({}, _default.options, options);
  1337. $.each(nodes, $.proxy(function (index, node) {
  1338. this._setChecked(node, true, options);
  1339. }, this));
  1340. };
  1341. /**
  1342. Uncheck all tree nodes
  1343. @param {optional Object} options
  1344. */
  1345. Tree.prototype.uncheckAll = function (options) {
  1346. options = $.extend({}, _default.options, options);
  1347. var nodes = $.grep(this._orderedNodes, function (node) {
  1348. return node.state.checked || node.state.checked === undefined;
  1349. });
  1350. $.each(nodes, $.proxy(function (index, node) {
  1351. this._setChecked(node, false, options);
  1352. }, this));
  1353. };
  1354. /**
  1355. Uncheck given tree nodes
  1356. @param {Array} nodes - An array of nodes
  1357. @param {optional Object} options
  1358. */
  1359. Tree.prototype.uncheckNode = function (nodes, options) {
  1360. if (!(nodes instanceof Array)) {
  1361. nodes = [nodes];
  1362. }
  1363. options = $.extend({}, _default.options, options);
  1364. $.each(nodes, $.proxy(function (index, node) {
  1365. this._setChecked(node, false, options);
  1366. }, this));
  1367. };
  1368. /**
  1369. Toggles a node's checked state; checking if unchecked, unchecking if checked.
  1370. @param {Array} nodes - An array of nodes
  1371. @param {optional Object} options
  1372. */
  1373. Tree.prototype.toggleNodeChecked = function (nodes, options) {
  1374. if (!(nodes instanceof Array)) {
  1375. nodes = [nodes];
  1376. }
  1377. options = $.extend({}, _default.options, options);
  1378. $.each(nodes, $.proxy(function (index, node) {
  1379. this._toggleChecked(node, options);
  1380. }, this));
  1381. };
  1382. /**
  1383. Saves the current state of checkboxes as default, cleaning up any highlighted changes
  1384. */
  1385. Tree.prototype.unmarkCheckboxChanges = function () {
  1386. this._inheritCheckboxChanges();
  1387. $.each(this._nodes, function (index, node) {
  1388. node.$el.removeClass('node-check-changed');
  1389. });
  1390. };
  1391. /**
  1392. Disable all tree nodes
  1393. @param {optional Object} options
  1394. */
  1395. Tree.prototype.disableAll = function (options) {
  1396. options = $.extend({}, _default.options, options);
  1397. var nodes = this._findNodes('false', 'state.disabled');
  1398. $.each(nodes, $.proxy(function (index, node) {
  1399. this._setDisabled(node, true, options);
  1400. }, this));
  1401. };
  1402. /**
  1403. Disable given tree nodes
  1404. @param {Array} nodes - An array of nodes
  1405. @param {optional Object} options
  1406. */
  1407. Tree.prototype.disableNode = function (nodes, options) {
  1408. if (!(nodes instanceof Array)) {
  1409. nodes = [nodes];
  1410. }
  1411. options = $.extend({}, _default.options, options);
  1412. $.each(nodes, $.proxy(function (index, node) {
  1413. this._setDisabled(node, true, options);
  1414. }, this));
  1415. };
  1416. /**
  1417. Enable all tree nodes
  1418. @param {optional Object} options
  1419. */
  1420. Tree.prototype.enableAll = function (options) {
  1421. options = $.extend({}, _default.options, options);
  1422. var nodes = this._findNodes('true', 'state.disabled');
  1423. $.each(nodes, $.proxy(function (index, node) {
  1424. this._setDisabled(node, false, options);
  1425. }, this));
  1426. };
  1427. /**
  1428. Enable given tree nodes
  1429. @param {Array} nodes - An array of nodes
  1430. @param {optional Object} options
  1431. */
  1432. Tree.prototype.enableNode = function (nodes, options) {
  1433. if (!(nodes instanceof Array)) {
  1434. nodes = [nodes];
  1435. }
  1436. options = $.extend({}, _default.options, options);
  1437. $.each(nodes, $.proxy(function (index, node) {
  1438. this._setDisabled(node, false, options);
  1439. }, this));
  1440. };
  1441. /**
  1442. Toggles a node's disabled state; disabling is enabled, enabling if disabled.
  1443. @param {Array} nodes - An array of nodes
  1444. @param {optional Object} options
  1445. */
  1446. Tree.prototype.toggleNodeDisabled = function (nodes, options) {
  1447. if (!(nodes instanceof Array)) {
  1448. nodes = [nodes];
  1449. }
  1450. options = $.extend({}, _default.options, options);
  1451. $.each(nodes, $.proxy(function (index, node) {
  1452. this._setDisabled(node, !node.state.disabled, options);
  1453. }, this));
  1454. };
  1455. /**
  1456. Searches the tree for nodes (text) that match given criteria
  1457. @param {String} pattern - A given string to match against
  1458. @param {optional Object} options - Search criteria options
  1459. @return {Array} nodes - Matching nodes
  1460. */
  1461. Tree.prototype.search = function (pattern, options) {
  1462. options = $.extend({}, _default.searchOptions, options);
  1463. var previous = this._getSearchResults();
  1464. var results = [];
  1465. if (pattern && pattern.length > 0) {
  1466. if (options.exactMatch) {
  1467. pattern = '^' + pattern + '$';
  1468. }
  1469. var modifier = 'g';
  1470. if (options.ignoreCase) {
  1471. modifier += 'i';
  1472. }
  1473. results = this._findNodes(pattern, 'text', modifier);
  1474. }
  1475. // Clear previous results no longer matched
  1476. $.each(this._diffArray(results, previous), $.proxy(function (index, node) {
  1477. this._setSearchResult(node, false, options);
  1478. }, this));
  1479. // Set new results
  1480. $.each(this._diffArray(previous, results), $.proxy(function (index, node) {
  1481. this._setSearchResult(node, true, options);
  1482. }, this));
  1483. // Reveal hidden nodes
  1484. if (results && options.revealResults) {
  1485. this.revealNode(results);
  1486. }
  1487. this._triggerEvent('searchComplete', results, options);
  1488. return results;
  1489. };
  1490. /**
  1491. Clears previous search results
  1492. */
  1493. Tree.prototype.clearSearch = function (options) {
  1494. options = $.extend({}, { render: true }, options);
  1495. var results = $.each(this._getSearchResults(), $.proxy(function (index, node) {
  1496. this._setSearchResult(node, false, options);
  1497. }, this));
  1498. this._triggerEvent('searchCleared', results, options);
  1499. };
  1500. Tree.prototype._getSearchResults = function () {
  1501. return this._findNodes('true', 'searchResult');
  1502. };
  1503. Tree.prototype._diffArray = function (a, b) {
  1504. var diff = [];
  1505. $.grep(b, function (n) {
  1506. if ($.inArray(n, a) === -1) {
  1507. diff.push(n);
  1508. }
  1509. });
  1510. return diff;
  1511. };
  1512. /**
  1513. Find nodes that match a given criteria
  1514. @param {String} pattern - A given string to match against
  1515. @param {optional String} attribute - Attribute to compare pattern against
  1516. @param {optional String} modifier - Valid RegEx modifiers
  1517. @return {Array} nodes - Nodes that match your criteria
  1518. */
  1519. Tree.prototype._findNodes = function (pattern, attribute, modifier) {
  1520. attribute = attribute || 'text';
  1521. modifier = modifier || 'g';
  1522. return $.grep(this._orderedNodes, $.proxy(function (node) {
  1523. var val = this._getNodeValue(node, attribute);
  1524. if (typeof val === 'string') {
  1525. return val.match(new RegExp(pattern, modifier));
  1526. }
  1527. }, this));
  1528. };
  1529. /**
  1530. Recursive find for retrieving nested attributes values
  1531. All values are return as strings, unless invalid
  1532. @param {Object} obj - Typically a node, could be any object
  1533. @param {String} attr - Identifies an object property using dot notation
  1534. @return {String} value - Matching attributes string representation
  1535. */
  1536. Tree.prototype._getNodeValue = function (obj, attr) {
  1537. var index = attr.indexOf('.');
  1538. if (index > 0) {
  1539. var _obj = obj[attr.substring(0, index)];
  1540. var _attr = attr.substring(index + 1, attr.length);
  1541. return this._getNodeValue(_obj, _attr);
  1542. }
  1543. else {
  1544. if (obj.hasOwnProperty(attr) && obj[attr] !== undefined) {
  1545. return obj[attr].toString();
  1546. }
  1547. else {
  1548. return undefined;
  1549. }
  1550. }
  1551. };
  1552. /**
  1553. des:扩展bootstrap-treeview的编辑节点方法
  1554. 编辑节点
  1555. author:qlx 2017-3-31
  1556. */
  1557. Tree.prototype.editNode = function (identifiers, options) {
  1558. $.each(identifiers,$.proxy(function (i, node) {
  1559. this.setEditNode(node, options);
  1560. }, this));
  1561. this._setInitialStates({ nodes: this._tree }, 0);
  1562. this._render();
  1563. }
  1564. /**
  1565. * 编辑节点
  1566. */
  1567. Tree.prototype.setEditNode = function (node, options) {
  1568. if (options) {
  1569. $.extend(node, options);
  1570. };
  1571. };
  1572. var logError = function (message) {
  1573. if (window.console) {
  1574. window.console.error(message);
  1575. }
  1576. };
  1577. // Prevent against multiple instantiations,
  1578. // handle updates and method calls
  1579. $.fn[pluginName] = function (options, args) {
  1580. var result;
  1581. if (this.length == 0) {
  1582. throw "No element has been found!";
  1583. }
  1584. this.each(function () {
  1585. var _this = $.data(this, pluginName);
  1586. if (typeof options === 'string') {
  1587. if (!_this) {
  1588. logError('Not initialized, can not call method : ' + options);
  1589. }
  1590. else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') {
  1591. logError('No such method : ' + options);
  1592. }
  1593. else {
  1594. if (!(args instanceof Array)) {
  1595. args = [ args ];
  1596. }
  1597. result = _this[options].apply(_this, args);
  1598. }
  1599. }
  1600. else if (typeof options === 'boolean') {
  1601. result = _this;
  1602. }
  1603. else {
  1604. $.data(this, pluginName, new Tree(this, $.extend(true, {}, options)));
  1605. }
  1606. });
  1607. return result || this;
  1608. };
  1609. })(jQuery, window, document);