v-accordion.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. /**
  2. * vAccordion - AngularJS multi-level accordion component
  3. * @version v1.2.1
  4. * @link http://lukaszwatroba.github.io/v-accordion
  5. * @author Łukasz Wątroba <l@lukaszwatroba.com>
  6. * @license MIT License, http://www.opensource.org/licenses/MIT
  7. */
  8. (function (angular) {
  9. 'use strict';
  10. // Config
  11. angular.module('vAccordion.config', [])
  12. .constant('accordionConfig', {
  13. states: {
  14. expanded: 'is-expanded'
  15. }
  16. });
  17. // Modules
  18. angular.module('vAccordion.directives', []);
  19. angular.module('vAccordion',
  20. [
  21. 'vAccordion.config',
  22. 'vAccordion.directives'
  23. ]);
  24. // vAccordion directive
  25. angular.module('vAccordion.directives')
  26. .directive('vAccordion', vAccordionDirective);
  27. function vAccordionDirective () {
  28. return {
  29. restrict: 'E',
  30. transclude: true,
  31. controller: AccordionDirectiveController,
  32. scope: {
  33. control: '=?',
  34. allowMultiple: '=?multiple',
  35. expandCb: '&?onexpand',
  36. collapseCb: '&?oncollapse'
  37. },
  38. link: function (scope, iElement, iAttrs, ctrl, transclude) {
  39. transclude(scope.$parent, function(clone) {
  40. iElement.append(clone);
  41. });
  42. var protectedApiMethods = ['toggle', 'expand', 'collapse', 'expandAll', 'collapseAll'];
  43. function checkCustomControlAPIMethods () {
  44. angular.forEach(protectedApiMethods, function (iteratedMethodName) {
  45. if (scope.control[iteratedMethodName]) {
  46. throw new Error('The `' + iteratedMethodName + '` method can not be overwritten');
  47. }
  48. });
  49. }
  50. if (!angular.isDefined(scope.allowMultiple)) {
  51. scope.allowMultiple = angular.isDefined(iAttrs.multiple);
  52. }
  53. iAttrs.$set('role', 'tablist');
  54. if (scope.allowMultiple) {
  55. iAttrs.$set('aria-multiselectable', 'true');
  56. }
  57. if (angular.isDefined(scope.control)) {
  58. checkCustomControlAPIMethods();
  59. var mergedControl = angular.extend({}, scope.internalControl, scope.control);
  60. scope.control = scope.internalControl = mergedControl;
  61. }
  62. else {
  63. scope.control = scope.internalControl;
  64. }
  65. }
  66. };
  67. }
  68. // vAccordion directive controller
  69. function AccordionDirectiveController ($scope) {
  70. var ctrl = this;
  71. var isDisabled = false;
  72. $scope.panes = [];
  73. ctrl.hasExpandedPane = function () {
  74. var bool = false;
  75. for (var i = 0, length = $scope.panes.length; i < length; i++) {
  76. var iteratedPane = $scope.panes[i];
  77. if (iteratedPane.isExpanded) {
  78. bool = true;
  79. break;
  80. }
  81. }
  82. return bool;
  83. };
  84. ctrl.getPaneByIndex = function (index) {
  85. return $scope.panes[index];
  86. };
  87. ctrl.getPaneIndex = function (pane) {
  88. return $scope.panes.indexOf(pane);
  89. };
  90. ctrl.disable = function () {
  91. isDisabled = true;
  92. };
  93. ctrl.enable = function () {
  94. isDisabled = false;
  95. };
  96. ctrl.addPane = function (paneToAdd) {
  97. if (!$scope.allowMultiple) {
  98. if (ctrl.hasExpandedPane() && paneToAdd.isExpanded) {
  99. throw new Error('The `multiple` attribute can\'t be found');
  100. }
  101. }
  102. $scope.panes.push(paneToAdd);
  103. if (paneToAdd.isExpanded) {
  104. $scope.expandCb({ index: ctrl.getPaneIndex(paneToAdd), target: paneToAdd, });
  105. }
  106. };
  107. ctrl.focusNext = function () {
  108. var length = $scope.panes.length;
  109. for (var i = 0; i < length; i++) {
  110. var iteratedPane = $scope.panes[i];
  111. if (iteratedPane.isFocused) {
  112. var paneToFocusIndex = i + 1;
  113. if (paneToFocusIndex > $scope.panes.length - 1) {
  114. paneToFocusIndex = 0;
  115. }
  116. var paneToFocus = $scope.panes[paneToFocusIndex];
  117. paneToFocus.paneElement.find('v-pane-header')[0].focus();
  118. break;
  119. }
  120. }
  121. };
  122. ctrl.focusPrevious = function () {
  123. var length = $scope.panes.length;
  124. for (var i = 0; i < length; i++) {
  125. var iteratedPane = $scope.panes[i];
  126. if (iteratedPane.isFocused) {
  127. var paneToFocusIndex = i - 1;
  128. if (paneToFocusIndex < 0) {
  129. paneToFocusIndex = $scope.panes.length - 1;
  130. }
  131. var paneToFocus = $scope.panes[paneToFocusIndex];
  132. paneToFocus.paneElement.find('v-pane-header')[0].focus();
  133. break;
  134. }
  135. }
  136. };
  137. ctrl.toggle = function (paneToToggle) {
  138. if (isDisabled || !paneToToggle) { return; }
  139. if (!$scope.allowMultiple) {
  140. ctrl.collapseAll(paneToToggle);
  141. }
  142. paneToToggle.isExpanded = !paneToToggle.isExpanded;
  143. if (paneToToggle.isExpanded) {
  144. $scope.expandCb({ index: ctrl.getPaneIndex(paneToToggle) });
  145. } else {
  146. $scope.collapseCb({ index: ctrl.getPaneIndex(paneToToggle) });
  147. }
  148. };
  149. ctrl.expand = function (paneToExpand) {
  150. if (isDisabled || !paneToExpand) { return; }
  151. if (!$scope.allowMultiple) {
  152. ctrl.collapseAll(paneToExpand);
  153. }
  154. if (!paneToExpand.isExpanded) {
  155. paneToExpand.isExpanded = true;
  156. $scope.expandCb({ index: ctrl.getPaneIndex(paneToExpand) });
  157. }
  158. };
  159. ctrl.collapse = function (paneToCollapse) {
  160. if (isDisabled || !paneToCollapse) { return; }
  161. if (paneToCollapse.isExpanded) {
  162. paneToCollapse.isExpanded = false;
  163. $scope.collapseCb({ index: ctrl.getPaneIndex(paneToCollapse) });
  164. }
  165. };
  166. ctrl.expandAll = function () {
  167. if (isDisabled) { return; }
  168. if ($scope.allowMultiple) {
  169. angular.forEach($scope.panes, function (iteratedPane) {
  170. ctrl.expand(iteratedPane);
  171. });
  172. } else {
  173. throw new Error('The `multiple` attribute can\'t be found');
  174. }
  175. };
  176. ctrl.collapseAll = function (exceptionalPane) {
  177. if (isDisabled) { return; }
  178. angular.forEach($scope.panes, function (iteratedPane) {
  179. if (iteratedPane !== exceptionalPane) {
  180. ctrl.collapse(iteratedPane);
  181. }
  182. });
  183. };
  184. // API
  185. $scope.internalControl = {
  186. toggle: function (index) {
  187. ctrl.toggle( ctrl.getPaneByIndex(index) );
  188. },
  189. expand: function (index) {
  190. ctrl.expand( ctrl.getPaneByIndex(index) );
  191. },
  192. collapse: function (index) {
  193. ctrl.collapse( ctrl.getPaneByIndex(index) );
  194. },
  195. expandAll: ctrl.expandAll,
  196. collapseAll: ctrl.collapseAll
  197. };
  198. }
  199. AccordionDirectiveController.$inject = ['$scope'];
  200. // vPaneContent directive
  201. angular.module('vAccordion.directives')
  202. .directive('vPaneContent', vPaneContentDirective);
  203. function vPaneContentDirective () {
  204. return {
  205. restrict: 'E',
  206. require: '^vPane',
  207. transclude: true,
  208. template: '<div ng-transclude></div>',
  209. scope: {},
  210. link: function (scope, iElement, iAttrs) {
  211. iAttrs.$set('role', 'tabpanel');
  212. }
  213. };
  214. }
  215. // vPaneHeader directive
  216. angular.module('vAccordion.directives')
  217. .directive('vPaneHeader', vPaneHeaderDirective);
  218. function vPaneHeaderDirective () {
  219. return {
  220. restrict: 'E',
  221. require: ['^vPane', '^vAccordion'],
  222. transclude: true,
  223. template: '<div ng-transclude></div>',
  224. scope: {},
  225. link: function (scope, iElement, iAttrs, ctrls) {
  226. iAttrs.$set('role', 'tab');
  227. var paneCtrl = ctrls[0];
  228. var accordionCtrl = ctrls[1];
  229. iElement.on('click', function () {
  230. scope.$apply(function () {
  231. paneCtrl.toggle();
  232. });
  233. });
  234. iElement[0].onfocus = function () {
  235. paneCtrl.focusPane();
  236. };
  237. iElement[0].onblur = function () {
  238. paneCtrl.blurPane();
  239. };
  240. iElement.on('keydown', function (event) {
  241. if (event.keyCode === 32 || event.keyCode === 13) {
  242. scope.$apply(function () { paneCtrl.toggle(); });
  243. event.preventDefault();
  244. } else if (event.keyCode === 39) {
  245. scope.$apply(function () { accordionCtrl.focusNext(); });
  246. event.preventDefault();
  247. } else if (event.keyCode === 37) {
  248. scope.$apply(function () { accordionCtrl.focusPrevious(); });
  249. event.preventDefault();
  250. }
  251. });
  252. }
  253. };
  254. }
  255. // vPane directive
  256. angular.module('vAccordion.directives')
  257. .directive('vPane', vPaneDirective);
  258. function vPaneDirective ($timeout, $animate, accordionConfig) {
  259. return {
  260. restrict: 'E',
  261. require: '^vAccordion',
  262. transclude: true,
  263. controller: PaneDirectiveController,
  264. scope: {
  265. isExpanded: '=?expanded'
  266. },
  267. link: function (scope, iElement, iAttrs, accordionCtrl, transclude) {
  268. transclude(scope.$parent, function (clone) {
  269. iElement.append(clone);
  270. });
  271. if (!angular.isDefined(scope.isExpanded)) {
  272. scope.isExpanded = angular.isDefined(iAttrs.expanded);
  273. }
  274. var states = accordionConfig.states;
  275. var paneHeader = iElement.find('v-pane-header'),
  276. paneContent = iElement.find('v-pane-content'),
  277. paneInner = paneContent.find('div');
  278. if (!paneHeader[0]) {
  279. throw new Error('The `v-pane-header` directive can\'t be found');
  280. }
  281. if (!paneContent[0]) {
  282. throw new Error('The `v-pane-content` directive can\'t be found');
  283. }
  284. accordionCtrl.addPane(scope);
  285. scope.paneElement = iElement;
  286. scope.paneContentElement = paneContent;
  287. scope.paneInnerElement = paneInner;
  288. scope.accordionCtrl = accordionCtrl;
  289. function expand () {
  290. accordionCtrl.disable();
  291. paneContent[0].style.maxHeight = '0px';
  292. paneHeader.attr({
  293. 'aria-selected': 'true',
  294. 'tabindex': '0'
  295. });
  296. scope.$emit('vAccordion:onExpand');
  297. $timeout(function () {
  298. $animate.addClass(iElement, states.expanded)
  299. .then(function () {
  300. accordionCtrl.enable();
  301. paneContent[0].style.maxHeight = 'none';
  302. scope.$emit('vAccordion:onExpandAnimationEnd');
  303. });
  304. setTimeout(function () {
  305. paneContent[0].style.maxHeight = paneInner[0].offsetHeight + 'px';
  306. }, 0);
  307. }, 0);
  308. }
  309. function collapse () {
  310. accordionCtrl.disable();
  311. paneContent[0].style.maxHeight = paneInner[0].offsetHeight + 'px';
  312. paneHeader.attr({
  313. 'aria-selected': 'false',
  314. 'tabindex': '-1'
  315. });
  316. scope.$emit('vAccordion:onCollapse');
  317. $timeout(function () {
  318. $animate.removeClass(iElement, states.expanded)
  319. .then(function () {
  320. accordionCtrl.enable();
  321. scope.$emit('vAccordion:onCollapseAnimationEnd');
  322. });
  323. setTimeout(function () {
  324. paneContent[0].style.maxHeight = '0px';
  325. }, 0);
  326. }, 0);
  327. }
  328. if (scope.isExpanded) {
  329. iElement.addClass(states.expanded);
  330. paneContent[0].style.maxHeight = 'none';
  331. paneHeader.attr({
  332. 'aria-selected': 'true',
  333. 'tabindex': '0'
  334. });
  335. } else {
  336. paneContent[0].style.maxHeight = '0px';
  337. paneHeader.attr({
  338. 'aria-selected': 'false',
  339. 'tabindex': '-1'
  340. });
  341. }
  342. scope.$watch('isExpanded', function (newValue, oldValue) {
  343. if (newValue === oldValue) { return true; }
  344. if (newValue) { expand(); }
  345. else { collapse(); }
  346. });
  347. }
  348. };
  349. }
  350. vPaneDirective.$inject = ['$timeout', '$animate', 'accordionConfig'];
  351. // vPane directive controller
  352. function PaneDirectiveController ($scope) {
  353. var ctrl = this;
  354. ctrl.toggle = function () {
  355. if (!$scope.isAnimating) {
  356. $scope.accordionCtrl.toggle($scope);
  357. }
  358. };
  359. ctrl.focusPane = function () {
  360. $scope.isFocused = true;
  361. };
  362. ctrl.blurPane = function () {
  363. $scope.isFocused = false;
  364. };
  365. }
  366. PaneDirectiveController.$inject = ['$scope'];
  367. })(angular);