angular-multi-select-tree-0.1.0_1.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. /*
  2. The MIT License (MIT)
  3. Copyright (c) 2014 Muhammed Ashik
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. */
  20. /*jshint indent: 2 */
  21. /*global angular: false */
  22. (function () {
  23. 'use strict';
  24. angular.module('multi-select-tree', []);
  25. }());
  26. /*jshint indent: 2 */
  27. (function (){
  28. 'use strict';
  29. angular.module('multi-select-tree').run(['$templateCache', function($templateCache) {
  30. 'use strict';
  31. $templateCache.put('src/multi-select-tree.tpl.html',
  32. "<div class=\"tree-control\">\n" +
  33. "\n" +
  34. " <div class=\"tree-input form-control\" ng-click=\"onControlClicked($event)\">\n" +
  35. " <span ng-if=\"selectedItems.length == 0\" class=\"noselected-items\">\n" +
  36. // " <span ng-if=\"selectedItems.length == 0\" class=\"\">\n" +
  37. " <span ng-bind=\"defaultLabel\" class=\"pull-left\"></span>\n" +
  38. " </span>\n" +
  39. " <span ng-if=\"selectedItems.length > 0\" class=\"selected-items\">\n" +
  40. // " <span ng-if=\"selectedItems.length > 0\" class=\"\">\n" +
  41. // " <span ng-repeat=\"selectedItem in selectedItems\" class=\"selected-item pull-left\">{{selectedItem.name||selectedItem[transLabel]}} <span class=\"selected-item-close\"\n" +
  42. " <span ng-repeat=\"selectedItem in selectedItems\" class=\"selected-item\">{{selectedItem.name||selectedItem[transLabel]}} <span class=\"selected-item-close\"\n" +
  43. " ng-click=\"deselectItem(selectedItem, $event)\"></span></span>\n" +
  44. " <span class=\"caret\"></span>\n" +
  45. " </span>\n" +
  46. " <!-- <input type=\"text\" class=\"blend-in\" /> -->\n" +
  47. " <i class=\"caret pull-right\"></i>\n" +
  48. " </div>\n" +
  49. " <div class=\"tree-view\" ng-show=\"showTree\">\n" +
  50. " <div class=\"helper-container\">\n" +
  51. " <div class=\"line\" data-ng-if=\"switchView\">\n" +
  52. " <button type=\"button\" ng-click=\"switchCurrentView($event);\" class=\"helper-button\">{{switchViewLabel}}</button>\n" +
  53. " </div>\n" +
  54. " <div class=\"line\">\n" +
  55. " <input placeholder=\"查询...\" type=\"text\" ng-model=\"filterKeyword\" ng-click=\"onFilterClicked($event)\"\n" +
  56. " class=\"input-filter\">\n" +
  57. " <span class=\"clear-button\" ng-click=\"clearFilter($event)\"><span class=\"item-close\"></span></span>\n" +
  58. " </div>\n" +
  59. " </div>\n" +
  60. " <ul class=\"tree-container\">\n" +
  61. " <tree-item class=\"top-level\" ng-repeat=\"item in inputModel\" item=\"item\" ng-show=\"!item.isFiltered\"\n" +
  62. " use-callback=\"useCallback\" can-select-item=\"canSelectItem\"\n" +
  63. " multi-select=\"multiSelect\" item-selected=\"itemSelected(item)\"\n" +
  64. " on-active-item=\"onActiveItem(item)\" select-only-leafs=\"selectOnlyLeafs\" trans-label=\"labelProp\"></tree-item>\n" +
  65. " </ul>\n" +
  66. " </div>\n" +
  67. "</div>\n"
  68. );
  69. $templateCache.put('src/tree-item.tpl.html',
  70. "<li>\n" +
  71. " <div class=\"item-container\" ng-class=\"{active: item.isActive, selected: item.selected}\"\n" +
  72. " ng-click=\"clickSelectItem(item, $event)\" ng-mouseover=\"onMouseOver(item, $event)\">\n" +
  73. " <span ng-if=\"showExpand(item)\" class=\"expand\" ng-class=\"{'expand-opened': item.isExpanded}\"\n" +
  74. " ng-click=\"onExpandClicked(item, $event)\"></span>\n" +
  75. "\n" +
  76. " <div class=\"item-details\"><input class=\"tree-checkbox\" type=\"checkbox\" ng-if=\"showCheckbox()\"\n" +
  77. " ng-checked=\"item.selected\"/>{{item.name||item[transLabel]}}\n" +
  78. " </div>\n" +
  79. " </div>\n" +
  80. " <ul ng-repeat=\"child in item.children\" ng-if=\"item.isExpanded\">\n" +
  81. " <tree-item item=\"child\" item-selected=\"subItemSelected(item)\" use-callback=\"useCallback\"\n" +
  82. " can-select-item=\"canSelectItem\" multi-select=\"multiSelect\"\n" +
  83. " on-active-item=\"activeSubItem(item, $event)\" trans-label=\"transLabel\"></tree-item>\n" +
  84. " </ul>\n" +
  85. "</li>\n"
  86. );
  87. }]);
  88. }());
  89. /*global angular: false */
  90. (function () {
  91. 'use strict';
  92. var mainModule = angular.module('multi-select-tree');
  93. /**
  94. * Controller for multi select tree.
  95. */
  96. mainModule.controller('multiSelectTreeCtrl', [
  97. '$scope',
  98. '$document',
  99. '$timeout',
  100. function ($scope, $document, $timeout) {
  101. var activeItem;
  102. $scope.showTree = false;
  103. $scope.selectedItems = [];
  104. $scope.multiSelect = $scope.multiSelect || false;
  105. var _refreshDelayPromise;
  106. /**
  107. * refresh
  108. */
  109. $scope._refresh = function(refreshAttr) {
  110. if (refreshAttr !== undefined) {
  111. // Debounce
  112. // See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155
  113. // FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177
  114. if (_refreshDelayPromise) {
  115. $timeout.cancel(_refreshDelayPromise);
  116. }
  117. _refreshDelayPromise = $timeout(function() {
  118. $scope.refresh({
  119. items: $scope
  120. });
  121. }, $scope.refreshDelay);
  122. }
  123. };
  124. /**
  125. * Clicking on document will hide the tree.
  126. */
  127. function docClickHide() {
  128. closePopup();
  129. $scope.$apply();
  130. }
  131. /**
  132. * Closes the tree popup.
  133. */
  134. function closePopup() {
  135. $scope.showTree = false;
  136. if (activeItem) {
  137. activeItem.isActive = false;
  138. activeItem = undefined;
  139. }
  140. $document.off('click', docClickHide);
  141. }
  142. /**
  143. * Sets the active item.
  144. *
  145. * @param item the item element.
  146. */
  147. $scope.onActiveItem = function (item) {
  148. if (activeItem !== item) {
  149. if (activeItem) {
  150. activeItem.isActive = false;
  151. }
  152. activeItem = item;
  153. activeItem.isActive = true;
  154. }
  155. };
  156. /**
  157. * Copies the selectedItems in to output model.
  158. */
  159. $scope.refreshOutputModel = function () {
  160. var outModel;
  161. if($scope.multiSelect){
  162. outModel = angular.copy($scope.selectedItems);
  163. angular.forEach(outModel, function(item){
  164. delete item.selected;
  165. })
  166. $scope.outputModel = outModel;
  167. }else{
  168. if($scope.selectedItems.length>0){
  169. outModel = angular.copy($scope.selectedItems[0]);
  170. delete outModel.selected;
  171. }
  172. }
  173. $scope.outputModel = outModel;//angular.copy($scope.selectedItems);
  174. };
  175. /**
  176. * Refreshes the selected Items model.
  177. */
  178. $scope.refreshSelectedItems = function () {
  179. $scope.selectedItems = [];
  180. if ($scope.inputModel) {
  181. setSelectedChildren($scope.inputModel);
  182. }
  183. };
  184. /**
  185. * Iterates over children and sets the selected items.
  186. *
  187. * @param children the children element.
  188. */
  189. function setSelectedChildren(children) {
  190. for (var i = 0, len = children.length; i < len; i++) {
  191. if (!isItemSelected(children[i]) && children[i].selected === true) {
  192. $scope.selectedItems.push(children[i]);
  193. } else if (isItemSelected(children[i]) && children[i].selected === false) {
  194. children[i].selected = true;
  195. }
  196. if (children[i] && children[i].children) {
  197. setSelectedChildren(children[i].children);
  198. }
  199. }
  200. }
  201. /**
  202. * Checks of the item is already selected.
  203. *
  204. * @param item the item to be checked.
  205. * @return {boolean} if the item is already selected.
  206. */
  207. function isItemSelected(item) {
  208. var isSelected = false;
  209. if ($scope.selectedItems) {
  210. for (var i = 0; i < $scope.selectedItems.length; i++) {
  211. if ($scope.selectedItems[i].id === item.id) {
  212. isSelected = true;
  213. break;
  214. }
  215. }
  216. }
  217. return isSelected;
  218. }
  219. /**
  220. * Deselect the item.
  221. *
  222. * @param item the item element
  223. * @param $event
  224. */
  225. $scope.deselectItem = function (item, $event) {
  226. $event.stopPropagation();
  227. $scope.selectedItems.splice($scope.selectedItems.indexOf(item), 1);
  228. item.selected = false;
  229. this.refreshOutputModel();
  230. };
  231. /**
  232. * Swap the tree popup on control click event.
  233. *
  234. * @param $event the click event.
  235. */
  236. $scope.onControlClicked = function ($event) {
  237. $event.stopPropagation();
  238. $scope.showTree = !$scope.showTree;
  239. if ($scope.showTree) {
  240. $document.on('click', docClickHide);
  241. }
  242. };
  243. /**
  244. * Stop the event on filter clicked.
  245. *
  246. * @param $event the click event
  247. */
  248. $scope.onFilterClicked = function ($event) {
  249. $event.stopPropagation();
  250. };
  251. /**
  252. * Clears the filter text.
  253. *
  254. * @param $event the click event
  255. */
  256. $scope.clearFilter = function ($event) {
  257. $event.stopPropagation();
  258. $scope.filterKeyword = '';
  259. };
  260. /**
  261. * Wrapper function for can select item callback.
  262. *
  263. * @param item the item
  264. */
  265. $scope.canSelectItem = function (item) {
  266. return $scope.callback({
  267. item: item,
  268. selectedItems: $scope.selectedItems
  269. });
  270. };
  271. /**
  272. * The callback is used to switch the views.
  273. * based on the view type.
  274. *
  275. * @param $event the event object.
  276. */
  277. $scope.switchCurrentView = function ($event) {
  278. $event.stopPropagation();
  279. $scope.switchViewCallback({ scopeObj: $scope });
  280. };
  281. /**
  282. * Handles the item select event.
  283. *
  284. * @param item the selected item.
  285. */
  286. $scope.itemSelected = function (item) {
  287. if ($scope.useCallback && $scope.canSelectItem(item) === false || $scope.selectOnlyLeafs && item.children && item.children.length > 0) {
  288. return;
  289. }
  290. if (!$scope.multiSelect) {
  291. closePopup();
  292. for (var i = 0; i < $scope.selectedItems.length; i++) {
  293. $scope.selectedItems[i].selected = false;
  294. }
  295. item.selected = true;
  296. $scope.selectedItems = [];
  297. $scope.selectedItems.push(item);
  298. } else {
  299. item.selected = true;
  300. var indexOfItem = $scope.selectedItems.indexOf(item);
  301. if (isItemSelected(item)) {
  302. item.selected = false;
  303. $scope.selectedItems.splice(indexOfItem, 1);
  304. } else {
  305. $scope.getparentdata(item);
  306. $scope.selectedItems.push(item);
  307. }
  308. }
  309. this.refreshOutputModel();
  310. };
  311. }
  312. ]);
  313. /**
  314. * sortableItem directive.
  315. */
  316. mainModule.directive('multiSelectTree', function () {
  317. return {
  318. restrict: 'E',
  319. templateUrl: 'src/multi-select-tree.tpl.html',
  320. scope: {
  321. inputModel: '=',
  322. outputModel: '=?',
  323. multiSelect: '=?',
  324. switchView: '=?',
  325. switchViewLabel: '@',
  326. switchViewCallback: '&',
  327. selectOnlyLeafs: '=?',
  328. callback: '&',
  329. refresh: '&',
  330. getparentdata: '&',
  331. refreshDelay: '@',
  332. defaultLabel: '@',
  333. transLabel:'@'
  334. },
  335. link: function (scope, element, attrs) {
  336. if (attrs.callback) {
  337. scope.useCallback = true;
  338. }
  339. if (attrs.transLabel) {
  340. scope.labelProp = attrs.transLabel;
  341. }
  342. //refresh
  343. attrs.$observe('refreshDelay', function() {
  344. // $eval() is needed otherwise we get a string instead of a number
  345. var refreshDelay = scope.$eval(attrs.refreshDelay);
  346. scope.refreshDelay = refreshDelay !== undefined ? refreshDelay : 100;
  347. //refresh
  348. scope._refresh(attrs.refresh);
  349. });
  350. // watch for changes in input model as a whole
  351. // this on updates the multi-select when a user load a whole new input-model.
  352. scope.$watch('inputModel', function (newVal) {
  353. if (newVal) {
  354. scope.refreshSelectedItems();
  355. scope.refreshOutputModel();
  356. }
  357. });
  358. /**
  359. * Checks whether any of children match the keyword.
  360. *
  361. * @param item the parent item
  362. * @param keyword the filter keyword
  363. * @returns {boolean} false if matches.
  364. */
  365. function isChildrenFiltered(item, keyword) {
  366. var childNodes = getAllChildNodesFromNode(item, []);
  367. for (var i = 0, len = childNodes.length; i < len; i++) {
  368. if (childNodes[i][scope.labelProp||'name'].indexOf(keyword) !== -1) {
  369. //if (childNodes[i].name.toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
  370. return false;
  371. }
  372. }
  373. return true;
  374. }
  375. /**
  376. * Return all childNodes of a given node (as Array of Nodes)
  377. */
  378. function getAllChildNodesFromNode(node, childNodes) {
  379. for (var i = 0; i < node.children.length; i++) {
  380. childNodes.push(node.children[i]);
  381. // add the childNodes from the children if available
  382. getAllChildNodesFromNode(node.children[i], childNodes);
  383. }
  384. return childNodes;
  385. }
  386. scope.$watch('filterKeyword', function () {
  387. if (scope.filterKeyword !== undefined) {
  388. angular.forEach(scope.inputModel, function (item) {
  389. if (item[scope.labelProp||'name'].indexOf(scope.filterKeyword) !== -1) {
  390. //if (item.name.toLowerCase().indexOf(scope.filterKeyword.toLowerCase()) !== -1) {
  391. item.isFiltered = false;
  392. } else if (!isChildrenFiltered(item, scope.filterKeyword)) {
  393. item.isFiltered = false;
  394. } else {
  395. item.isFiltered = true;
  396. }
  397. });
  398. }
  399. });
  400. },
  401. controller: 'multiSelectTreeCtrl'
  402. };
  403. });
  404. }());
  405. /*jshint indent: 2 */
  406. /*global angular: false */
  407. (function () {
  408. 'use strict';
  409. var mainModule = angular.module('multi-select-tree');
  410. /**
  411. * Controller for sortable item.
  412. *
  413. * @param $scope - drag item scope
  414. */
  415. mainModule.controller('treeItemCtrl', [
  416. '$scope',
  417. function ($scope) {
  418. // $scope.item.isExpanded = false;
  419. /**
  420. * Shows the expand option.
  421. *
  422. * @param item the item
  423. * @returns {*|boolean}
  424. */
  425. $scope.showExpand = function (item) {
  426. return item.children && item.children.length > 0;
  427. };
  428. /**
  429. * On expand clicked toggle the option.
  430. *
  431. * @param item the item
  432. * @param $event
  433. */
  434. $scope.onExpandClicked = function (item, $event) {
  435. $event.stopPropagation();
  436. item.isExpanded = !item.isExpanded;
  437. };
  438. /**
  439. * Event on click of select item.
  440. *
  441. * @param item the item
  442. * @param $event
  443. */
  444. $scope.clickSelectItem = function (item, $event) {
  445. $event.stopPropagation();
  446. item.isExpanded = !item.isExpanded;
  447. if ($scope.itemSelected) {
  448. $scope.itemSelected({ item: item });
  449. }
  450. };
  451. /**
  452. * Is leaf selected.
  453. *
  454. * @param item the item
  455. * @param $event
  456. */
  457. $scope.subItemSelected = function (item, $event) {
  458. if ($scope.itemSelected) {
  459. $scope.itemSelected({ item: item });
  460. }
  461. };
  462. /**
  463. * Active sub item.
  464. *
  465. * @param item the item
  466. * @param $event
  467. */
  468. $scope.activeSubItem = function (item, $event) {
  469. if ($scope.onActiveItem) {
  470. $scope.onActiveItem({ item: item });
  471. }
  472. };
  473. /**
  474. * On mouse over event.
  475. *
  476. * @param item the item
  477. * @param $event
  478. */
  479. $scope.onMouseOver = function (item, $event) {
  480. $event.stopPropagation();
  481. if ($scope.onActiveItem) {
  482. $scope.onActiveItem({ item: item });
  483. }
  484. };
  485. /**
  486. * Can select item.
  487. *
  488. * @returns {*}
  489. */
  490. $scope.showCheckbox = function () {
  491. if (!$scope.multiSelect) {
  492. return false;
  493. }
  494. if ($scope.selectOnlyLeafs) {
  495. return false;
  496. }
  497. if ($scope.useCallback) {
  498. return $scope.canSelectItem($scope.item);
  499. }
  500. };
  501. }
  502. ]);
  503. /**
  504. * sortableItem directive.
  505. */
  506. mainModule.directive('treeItem', [
  507. '$compile',
  508. function ($compile) {
  509. return {
  510. restrict: 'E',
  511. templateUrl: 'src/tree-item.tpl.html',
  512. scope: {
  513. item: '=',
  514. itemSelected: '&',
  515. onActiveItem: '&',
  516. multiSelect: '=?',
  517. selectOnlyLeafs: '=?',
  518. isActive: '=',
  519. useCallback: '=',
  520. canSelectItem: '=',
  521. transLabel:'='
  522. },
  523. controller: 'treeItemCtrl',
  524. compile: function (element, attrs, link) {
  525. // Normalize the link parameter
  526. if (angular.isFunction(link)) {
  527. link = { post: link };
  528. }
  529. // Break the recursion loop by removing the contents
  530. var contents = element.contents().remove();
  531. var compiledContents;
  532. return {
  533. pre: link && link.pre ? link.pre : null,
  534. post: function (scope, element, attrs) {
  535. // Compile the contents
  536. if (!compiledContents) {
  537. compiledContents = $compile(contents);
  538. }
  539. // Re-add the compiled contents to the element
  540. compiledContents(scope, function (clone) {
  541. element.append(clone);
  542. });
  543. // Call the post-linking function, if any
  544. if (link && link.post) {
  545. link.post.apply(null, arguments);
  546. }
  547. }
  548. };
  549. }
  550. };
  551. }
  552. ]);
  553. }());