├── .bowerrc
├── .editorconfig
├── .gitignore
├── .jshintrc
├── .yo-rc.json
├── LICENSE
├── README.md
├── bower.json
├── demo
└── app.js
├── dist
├── v-accordion.css
├── v-accordion.js
├── v-accordion.min.css
└── v-accordion.min.js
├── gulpfile.js
├── index.html
├── index.js
├── karma.conf.js
├── package.json
├── src
└── vAccordion
│ ├── directives
│ ├── vAccordion.js
│ ├── vPane.js
│ ├── vPaneContent.js
│ └── vPaneHeader.js
│ ├── styles
│ ├── _base.scss
│ ├── _settings.scss
│ ├── _theme.scss
│ └── vAccordion.scss
│ ├── vAccordion.js
│ ├── vAccordion.prefix
│ └── vAccordion.suffix
└── test
└── unit
└── vAccordion
├── controllers
├── vAccordionController.spec.js
└── vPaneController.spec.js
├── directives
├── vAccordion.spec.js
├── vPane.spec.js
├── vPaneContent.spec.js
└── vPaneHeader.spec.js
└── vAccordion.spec.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower"
3 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ._*
2 | .~lock.*
3 | .buildpath
4 | .DS_Store
5 | .idea
6 | .project
7 | .settings
8 |
9 | # Ignore node stuff
10 | node_modules/
11 | npm-debug.log
12 | libpeerconnection.log
13 |
14 | # OS-specific
15 | .DS_Store
16 |
17 | # Bower components
18 | bower
19 |
20 | # Sublime
21 | *.sublime-*
22 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "esnext": true,
4 | "bitwise": true,
5 | "camelcase": true,
6 | "curly": true,
7 | "eqeqeq": true,
8 | "immed": true,
9 | "indent": 2,
10 | "latedef": false,
11 | "newcap": true,
12 | "noarg": true,
13 | "quotmark": "single",
14 | "regexp": true,
15 | "undef": true,
16 | "unused": true,
17 | "strict": false,
18 | "trailing": true,
19 | "smarttabs": true,
20 | "white": true,
21 | "validthis": true,
22 |
23 | "jasmine": true,
24 |
25 | "globals": {
26 | "angular": false
27 | },
28 |
29 | "predef": [
30 | "inject"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-angularjs-library": {
3 | "props": {
4 | "author": {
5 | "name": "Łukasz Wątroba",
6 | "email": "l@lukaszwatroba.com"
7 | },
8 | "libraryName": {
9 | "original": "v-accordion",
10 | "camelized": "vAccordion",
11 | "dasherized": "v-accordion",
12 | "slugified": "v-accordion",
13 | "parts": [
14 | "v",
15 | "accordion"
16 | ]
17 | },
18 | "includeModuleDirectives": false,
19 | "includeModuleFilters": false,
20 | "includeModuleServices": false,
21 | "includeAngularModuleResource": false,
22 | "includeAngularModuleCookies": false,
23 | "includeAngularModuleSanitize": false,
24 | "librarySrcDirectory": "src/vAccordion",
25 | "libraryUnitTestDirectory": "test/unit/vAccordion",
26 | "libraryUnitE2eDirectory": "test/e2e/vAccordion"
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2014 Łukasz Wątroba
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # AngularJS multi-level accordion
3 |
4 | - Allows for a nested structure
5 | - Works with (or without) `ng-repeat`
6 | - Allows multiple sections to be open at once
7 |
8 |
9 | ## Examples
10 |
11 | - [GitHub](http://lukaszwatroba.github.io/v-accordion)
12 | - [CodePen](http://codepen.io/LukaszWatroba/pen/MwdaLo)
13 | - [Linksfridge](https://linksfridge.com/help)
14 |
15 |
16 | ## Usage
17 |
18 | - If you use [bower](http://bower.io/) or [npm](https://www.npmjs.com/), just `bower/npm install v-accordion`. If not, download files [from the github repo](./dist).
19 |
20 | - Include `angular.js`, `angular-animate.js`, `v-accordion.js`, and `v-accordion.css`:
21 | ```html
22 |
23 |
24 |
25 |
26 |
27 |
28 | ```
29 |
30 | - Add `vAccordion` and `ngAnimate` as dependencies to your application module:
31 | ```js
32 | angular.module('myApp', ['vAccordion', 'ngAnimate']);
33 | ```
34 |
35 | - Put the following markup in your template:
36 | ```html
37 |
38 |
39 |
40 |
41 |
42 |
43 | Pane header #1
44 |
45 |
46 |
47 | Pane content #1
48 |
49 |
50 |
51 |
52 |
53 | Pane header #2
54 |
55 |
56 |
57 | Pane content #2
58 |
59 |
60 |
61 |
62 | ```
63 |
64 | - You can also use `v-accordion` with `ng-repeat`:
65 | ```html
66 |
67 |
68 |
69 |
70 | {{ ::pane.header }}
71 |
72 |
73 |
74 | {{ ::pane.content }}
75 |
76 |
77 |
78 |
79 |
80 | {{ ::subpane.header }}
81 |
82 |
83 | {{ ::subpane.content }}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | ```
92 |
93 |
94 | ## API
95 |
96 | #### Control
97 |
98 | Add `control` attribute and use the following methods to control vAccordion from it's parent scope:
99 |
100 | - `toggle(indexOrId)`
101 | - `expand(indexOrId)`
102 | - `collapse(indexOrId)`
103 | - `expandAll()`
104 | - `collapseAll()`
105 | - `hasExpandedPane()`
106 |
107 | ```html
108 |
109 |
110 |
111 |
112 | {{ ::pane.header }}
113 |
114 |
115 |
116 | {{ ::pane.content }}
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | ```
126 |
127 | ```js
128 | $scope.$on('my-accordion:onReady', function () {
129 | var firstPane = $scope.panes[0];
130 | $scope.accordion.toggle(firstPane.id);
131 | });
132 | ```
133 |
134 | #### Scope
135 |
136 | `$accordion` and `$pane` properties allows you to control the directive from it's transcluded scope.
137 |
138 | ##### $accordion
139 |
140 | - `toggle(indexOrId)`
141 | - `expand(indexOrId)`
142 | - `collapse(indexOrId)`
143 | - `expandAll()`
144 | - `collapseAll()`
145 | - `hasExpandedPane()`
146 | - `id`
147 |
148 | ##### $pane
149 |
150 | - `toggle()`
151 | - `expand()`
152 | - `collapse()`
153 | - `isExpanded()`
154 | - `id`
155 |
156 | ```html
157 |
158 |
159 |
160 |
161 |
162 | {{ ::pane.header }}
163 |
164 |
165 |
166 |
167 | {{ ::pane.content }}
168 |
169 |
170 |
171 |
172 |
173 |
174 | ```
175 |
176 |
177 | #### Events
178 |
179 | The directive emits the following events:
180 |
181 | - `vAccordion:onReady` or `yourAccordionId:onReady`
182 | - `vAccordion:onExpand` or `yourAccordionId:onExpand`
183 | - `vAccordion:onExpandAnimationEnd` or `yourAccordionId:onExpandAnimationEnd`
184 | - `vAccordion:onCollapse` or `yourAccordionId:onCollapse`
185 | - `vAccordion:onCollapseAnimationEnd` or `yourAccordionId:onCollapseAnimationEnd`
186 |
187 |
188 | ## Callbacks
189 |
190 | Use these callbacks to get the expanded/collapsed pane index and id:
191 |
192 | ```html
193 |
194 |
195 |
196 |
197 | {{ ::pane.header }}
198 |
199 |
200 |
201 | {{ ::pane.content }}
202 |
203 |
204 |
205 |
206 | ```
207 |
208 | ```js
209 | $scope.expandCallback = function (index, id) {
210 | console.log('expanded pane:', index, id);
211 | };
212 |
213 | $scope.collapseCallback = function (index, id) {
214 | console.log('collapsed pane:', index, id);
215 | };
216 | ```
217 |
218 |
219 | ## Configuration
220 |
221 | #### Module
222 | To change the default animation duration, inject `accordionConfig` provider in your app config:
223 |
224 | ```javascript
225 | angular
226 | .module('myApp', ['vAccordion'])
227 | .config(function (accordionConfig) {
228 | accordionConfig.expandAnimationDuration = 0.5;
229 | });
230 | ```
231 |
232 | #### SCSS
233 | If you are using SASS, you can import vAccordion.scss file and override the following variables:
234 |
235 | ```scss
236 | $v-accordion-default-theme: true !default;
237 |
238 | $v-accordion-spacing: 20px !default;
239 |
240 | $v-pane-border-color: #D8D8D8 !default;
241 | $v-pane-expanded-border-color: #2196F3 !default;
242 | $v-pane-icon-color: #2196F3 !default;
243 | $v-pane-hover-color: #2196F3 !default;
244 |
245 | $v-pane-disabled-opacity: 0.6 !default;
246 |
247 | $v-pane-expand-animation-duration: 0.5s !default;
248 | $v-pane-hover-animation-duration: 0.25s !default;
249 | ```
250 |
251 |
252 | ## Accessibility
253 | vAccordion manages keyboard focus and adds some common aria-* attributes. BUT you should additionally place the `aria-controls` and `aria-labelledby` as follows:
254 |
255 | ```html
256 |
257 |
258 |
259 |
262 |
263 |
264 | {{ ::pane.content }}
265 |
266 |
267 |
268 |
269 | ```
270 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v-accordion",
3 | "description": "vAccordion - AngularJS multi-level accordion component",
4 | "keywords": [
5 | "AngularJS",
6 | "accordion",
7 | "multi-level",
8 | "component",
9 | "directive",
10 | "module"
11 | ],
12 | "authors": [
13 | "Łukasz Wątroba "
14 | ],
15 | "main": [
16 | "dist/v-accordion.js",
17 | "dist/v-accordion.css"
18 | ],
19 | "dependencies": {
20 | "angular": "^1.3.0",
21 | "angular-animate": "^1.3.0"
22 | },
23 | "devDependencies": {
24 | "angular-mocks": "~1.4.0",
25 | "angular-scenario": "~1.4.0",
26 | "valitycss": "~0.3.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | angular
5 | .module('myApp', [ 'ngAnimate', 'vAccordion' ])
6 |
7 | .controller('MainController', function ($scope) {
8 |
9 | $scope.panesA = [
10 | {
11 | id: 'pane-1a',
12 | header: 'Pane 1',
13 | content: 'Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. Vestibulum dapibus, mauris nec malesuada fames ac turpis velit, rhoncus eu, luctus et interdum adipiscing wisi.',
14 | isExpanded: true
15 | },
16 | {
17 | id: 'pane-2a',
18 | header: 'Pane 2',
19 | content: 'Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies.'
20 | },
21 | {
22 | id: 'pane-3a',
23 | header: 'Pane 3',
24 | content: 'Aliquam erat ac ipsum. Integer aliquam purus. Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non.',
25 |
26 | subpanes: [
27 | {
28 | id: 'subpane-1a',
29 | header: 'Subpane 1',
30 | content: 'Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non.'
31 | },
32 | {
33 | id: 'subpane-2a',
34 | header: 'Subpane 2 (disabled)',
35 | content: 'Curabitur et ligula. Ut molestie a, ultricies porta urna. Quisque lorem tortor fringilla sed, vestibulum id.',
36 | isDisabled: true
37 | }
38 | ]
39 | }
40 | ];
41 |
42 | $scope.panesB = [
43 | {
44 | id: 'pane-1b',
45 | header: 'Pane 1',
46 | content: 'Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. Vestibulum dapibus, mauris nec malesuada fames ac turpis velit, rhoncus eu, luctus et interdum adipiscing wisi.',
47 | isExpanded: true
48 | },
49 | {
50 | id: 'pane-2b',
51 | header: 'Pane 2',
52 | content: 'Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies.'
53 | },
54 | {
55 | id: 'pane-3b',
56 | header: 'Pane 3',
57 | content: 'Aliquam erat ac ipsum. Integer aliquam purus. Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non.',
58 |
59 | subpanes: [
60 | {
61 | id: 'subpane-1b',
62 | header: 'Subpane 1',
63 | content: 'Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non.'
64 | },
65 | {
66 | id: 'subpane-2b',
67 | header: 'Subpane 2 (disabled)',
68 | content: 'Curabitur et ligula. Ut molestie a, ultricies porta urna. Quisque lorem tortor fringilla sed, vestibulum id.',
69 | isDisabled: true
70 | }
71 | ]
72 | }
73 | ];
74 |
75 | $scope.expandCallback = function (index, id) {
76 | console.log('expand:', index, id);
77 | };
78 |
79 | $scope.collapseCallback = function (index, id) {
80 | console.log('collapse:', index, id);
81 | };
82 |
83 | $scope.$on('accordionA:onReady', function () {
84 | console.log('accordionA is ready!');
85 | });
86 |
87 | });
88 |
89 | })(angular);
90 |
--------------------------------------------------------------------------------
/dist/v-accordion.css:
--------------------------------------------------------------------------------
1 | /**
2 | * vAccordion - AngularJS multi-level accordion component
3 | * @version v1.6.0
4 | * @link http://lukaszwatroba.github.io/v-accordion
5 | * @author Łukasz Wątroba
6 | * @license MIT License, http://www.opensource.org/licenses/MIT
7 | */
8 |
9 | /***************************************
10 | vAccordion
11 | ***************************************/
12 | /**
13 | * Example HTML:
14 | *
15 |
16 |
17 |
20 |
21 | [content]
22 |
23 |
24 |
25 | */
26 | /* Base styles
27 | ***************************************/
28 | v-accordion {
29 | display: block; }
30 |
31 | v-pane {
32 | display: block; }
33 | v-pane.is-expanded > v-pane-content > div {
34 | display: visible; }
35 | v-pane[disabled] > v-pane-header {
36 | opacity: 0.6;
37 | pointer-events: none; }
38 |
39 | v-pane-header {
40 | display: block;
41 | position: relative;
42 | cursor: pointer;
43 | -webkit-user-select: none;
44 | -moz-user-select: none;
45 | -ms-user-select: none;
46 | user-select: none;
47 | outline: none; }
48 | v-pane-header:focus {
49 | outline: none; }
50 | v-pane-header > div {
51 | display: block; }
52 |
53 | v-pane-content {
54 | display: block;
55 | position: relative;
56 | overflow: hidden;
57 | max-height: 0px; }
58 | v-pane-content > div {
59 | visibility: none; }
60 |
61 | /* Theme: default
62 | ***************************************/
63 | .vAccordion--default v-accordion {
64 | margin-top: 20px;
65 | padding-left: 20px; }
66 |
67 | .vAccordion--default v-pane-content > div {
68 | padding-bottom: 20px;
69 | opacity: 0;
70 | -webkit-transform: translate3d(0, 30px, 0);
71 | transform: translate3d(0, 30px, 0);
72 | -webkit-transition: all 0.5s;
73 | transition: all 0.5s; }
74 |
75 | .vAccordion--default v-pane {
76 | overflow: hidden; }
77 | .vAccordion--default v-pane.is-expanded > v-pane-header {
78 | border-bottom-color: #2196F3; }
79 | .vAccordion--default v-pane.is-expanded > v-pane-header::after {
80 | -webkit-transform: rotate(90deg);
81 | transform: rotate(90deg);
82 | opacity: 0; }
83 | .vAccordion--default v-pane.is-expanded > v-pane-header::before {
84 | -webkit-transform: rotate(0deg);
85 | transform: rotate(0deg); }
86 | .vAccordion--default v-pane.is-expanded > v-pane-content > div {
87 | opacity: 1;
88 | -webkit-transform: translate3d(0, 0, 0);
89 | transform: translate3d(0, 0, 0); }
90 | .vAccordion--default v-pane[disabled] v-pane-header::after, .vAccordion--default v-pane[disabled] v-pane-header::before {
91 | display: none; }
92 |
93 | .vAccordion--default v-pane-header {
94 | padding: 5px 0;
95 | margin-bottom: 20px;
96 | border-bottom: 2px solid #D8D8D8;
97 | -webkit-transition: all 0.25s;
98 | transition: all 0.25s; }
99 | .vAccordion--default v-pane-header::after, .vAccordion--default v-pane-header::before {
100 | content: '';
101 | display: block;
102 | position: absolute;
103 | top: 50%;
104 | right: 0;
105 | width: 10px;
106 | height: 1px;
107 | background-color: #2196F3;
108 | -webkit-transform-origin: 50% 50%;
109 | transform-origin: 50% 50%;
110 | will-change: transform;
111 | -webkit-transition: all 0.25s;
112 | transition: all 0.25s; }
113 | .vAccordion--default v-pane-header::before {
114 | -webkit-transform: rotate(-90deg);
115 | transform: rotate(-90deg); }
116 | .vAccordion--default v-pane-header:hover, .vAccordion--default v-pane-header:focus {
117 | color: #2196F3; }
118 |
--------------------------------------------------------------------------------
/dist/v-accordion.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vAccordion - AngularJS multi-level accordion component
3 | * @version v1.6.0
4 | * @link http://lukaszwatroba.github.io/v-accordion
5 | * @author Łukasz Wątroba
6 | * @license MIT License, http://www.opensource.org/licenses/MIT
7 | */
8 |
9 | (function (angular) {
10 | 'use strict';
11 |
12 | // Config
13 | angular.module('vAccordion.config', [])
14 | .constant('accordionConfig', {
15 | states: {
16 | expanded: 'is-expanded'
17 | },
18 | expandAnimationDuration: 0.5
19 | })
20 | .animation('.is-expanded', [ '$animateCss', 'accordionConfig', function ($animateCss, accordionConfig) {
21 | return {
22 | addClass: function (element, className, done) {
23 | var paneContent = angular.element(element[0].querySelector('v-pane-content')),
24 | paneInner = angular.element(paneContent[0].querySelector('div'));
25 |
26 | var height = paneInner[0].offsetHeight;
27 |
28 | var expandAnimation = $animateCss(paneContent, {
29 | easing: 'ease',
30 | from: { maxHeight: '0px' },
31 | to: { maxHeight: height + 'px' },
32 | duration: accordionConfig.expandAnimationDuration
33 | });
34 |
35 | expandAnimation.start().done(function () {
36 | paneContent.css('max-height', 'none');
37 | done();
38 | });
39 |
40 | return function (isCancelled) {
41 | if (isCancelled) {
42 | paneContent.css('max-height', 'none');
43 | }
44 | };
45 | },
46 | removeClass: function (element, className, done) {
47 | var paneContent = angular.element(element[0].querySelector('v-pane-content')),
48 | paneInner = angular.element(paneContent[0].querySelector('div'));
49 |
50 | var height = paneInner[0].offsetHeight;
51 |
52 | var collapseAnimation = $animateCss(paneContent, {
53 | easing: 'ease',
54 | from: { maxHeight: height + 'px' },
55 | to: { maxHeight: '0px' },
56 | duration: accordionConfig.expandAnimationDuration
57 | });
58 |
59 | collapseAnimation.start().done(done);
60 |
61 | return function (isCancelled) {
62 | if (isCancelled) {
63 | paneContent.css('max-height', '0px');
64 | }
65 | };
66 | }
67 | };
68 | } ]);
69 |
70 |
71 | // Modules
72 | angular.module('vAccordion.directives', []);
73 | angular.module('vAccordion',
74 | [
75 | 'vAccordion.config',
76 | 'vAccordion.directives'
77 | ]);
78 |
79 |
80 |
81 | // vAccordion directive
82 | angular.module('vAccordion.directives')
83 | .directive('vAccordion', vAccordionDirective);
84 |
85 |
86 | function vAccordionDirective ($timeout) {
87 | return {
88 | restrict: 'E',
89 | transclude: true,
90 | controller: vAccordionController,
91 | scope: {
92 | control: '=?',
93 | expandCb: '&?onexpand',
94 | collapseCb: '&?oncollapse',
95 | id: '@?'
96 | },
97 | link: {
98 | pre: function (scope, iElement, iAttrs) {
99 | scope.allowMultiple = (angular.isDefined(iAttrs.multiple) && (iAttrs.multiple === '' || iAttrs.multiple === 'true'));
100 | },
101 | post: function (scope, iElement, iAttrs, ctrl, transclude) {
102 | transclude(scope.$parent.$new(), function (clone, transclusionScope) {
103 | transclusionScope.$accordion = scope.internalControl;
104 | if (scope.id) { transclusionScope.$accordion.id = scope.id; }
105 | iElement.append(clone);
106 | });
107 |
108 | iAttrs.$set('role', 'tablist');
109 |
110 | if (scope.allowMultiple) {
111 | iAttrs.$set('aria-multiselectable', 'true');
112 | }
113 |
114 | if (angular.isDefined(scope.control)) {
115 | checkCustomControlAPIMethods();
116 |
117 | var mergedControl = angular.extend({}, scope.internalControl, scope.control);
118 | scope.control = scope.internalControl = mergedControl;
119 | }
120 | else {
121 | scope.control = scope.internalControl;
122 | }
123 |
124 | function checkCustomControlAPIMethods () {
125 | var protectedApiMethods = ['toggle', 'expand', 'collapse', 'expandAll', 'collapseAll', 'hasExpandedPane'];
126 |
127 | angular.forEach(protectedApiMethods, function (iteratedMethodName) {
128 | if (scope.control[iteratedMethodName]) {
129 | throw new Error('The `' + iteratedMethodName + '` method can not be overwritten');
130 | }
131 | });
132 | }
133 |
134 | $timeout(function () {
135 | var eventName = (angular.isDefined(ctrl.getAccordionId())) ? ctrl.getAccordionId() + ':onReady' : 'vAccordion:onReady';
136 | scope.$emit(eventName);
137 | }, 0);
138 | }
139 | }
140 | };
141 | }
142 | vAccordionDirective.$inject = ['$timeout'];
143 |
144 |
145 | // vAccordion directive controller
146 | function vAccordionController ($scope) {
147 | var ctrl = this;
148 | var isDisabled = false;
149 |
150 | $scope.panes = [];
151 |
152 | $scope.expandCb = (angular.isFunction($scope.expandCb)) ? $scope.expandCb : angular.noop;
153 | $scope.collapseCb = (angular.isFunction($scope.collapseCb)) ? $scope.collapseCb : angular.noop;
154 |
155 | ctrl.hasExpandedPane = function hasExpandedPane () {
156 | var bool = false;
157 |
158 | for (var i = 0, length = $scope.panes.length; i < length; i++) {
159 | var iteratedPane = $scope.panes[i];
160 |
161 | if (iteratedPane.isExpanded) {
162 | bool = true;
163 | break;
164 | }
165 | }
166 |
167 | return bool;
168 | };
169 |
170 | ctrl.getPaneByIndex = function getPaneByIndex (index) {
171 | var thePane;
172 |
173 | angular.forEach($scope.panes, function (iteratedPane) {
174 | if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane.$parent.$index === index) {
175 | thePane = iteratedPane;
176 | }
177 | });
178 |
179 | return (thePane) ? thePane : $scope.panes[index];
180 | };
181 |
182 | ctrl.getPaneIndex = function getPaneIndex (pane) {
183 | var theIndex;
184 |
185 | angular.forEach($scope.panes, function (iteratedPane) {
186 | if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane === pane) {
187 | theIndex = iteratedPane.$parent.$index;
188 | }
189 | });
190 |
191 | return (angular.isDefined(theIndex)) ? theIndex : $scope.panes.indexOf(pane);
192 | };
193 |
194 | ctrl.getPaneById = function getPaneById (id) {
195 | var thePane;
196 |
197 | angular.forEach($scope.panes, function (iteratedPane) {
198 | if (iteratedPane.id && iteratedPane.id === id) {
199 | thePane = iteratedPane;
200 | }
201 | });
202 |
203 | return thePane;
204 | };
205 |
206 | ctrl.getPaneId = function getPaneId (pane) {
207 | return pane.id;
208 | };
209 |
210 | ctrl.getAccordionId = function getAccordionId () {
211 | return $scope.id;
212 | };
213 |
214 |
215 | ctrl.disable = function disable () {
216 | isDisabled = true;
217 | };
218 |
219 | ctrl.enable = function enable () {
220 | isDisabled = false;
221 | };
222 |
223 | ctrl.addPane = function addPane (paneToAdd) {
224 | if (!$scope.allowMultiple) {
225 | if (ctrl.hasExpandedPane() && paneToAdd.isExpanded) {
226 | throw new Error('The `multiple` attribute can\'t be found');
227 | }
228 | }
229 |
230 | $scope.panes.push(paneToAdd);
231 |
232 | if (paneToAdd.isExpanded) {
233 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToAdd), id: paneToAdd.id, pane: paneToAdd });
234 | }
235 | };
236 |
237 | ctrl.focusNext = function focusNext () {
238 | var length = $scope.panes.length;
239 |
240 | for (var i = 0; i < length; i++) {
241 | var iteratedPane = $scope.panes[i];
242 |
243 | if (iteratedPane.isFocused) {
244 | var paneToFocusIndex = i + 1;
245 |
246 | if (paneToFocusIndex > $scope.panes.length - 1) {
247 | paneToFocusIndex = 0;
248 | }
249 |
250 | var paneToFocus = $scope.panes[paneToFocusIndex];
251 | paneToFocus.paneElement.find('v-pane-header')[0].focus();
252 |
253 | break;
254 | }
255 | }
256 | };
257 |
258 | ctrl.focusPrevious = function focusPrevious () {
259 | var length = $scope.panes.length;
260 |
261 | for (var i = 0; i < length; i++) {
262 | var iteratedPane = $scope.panes[i];
263 |
264 | if (iteratedPane.isFocused) {
265 | var paneToFocusIndex = i - 1;
266 |
267 | if (paneToFocusIndex < 0) {
268 | paneToFocusIndex = $scope.panes.length - 1;
269 | }
270 |
271 | var paneToFocus = $scope.panes[paneToFocusIndex];
272 | paneToFocus.paneElement.find('v-pane-header')[0].focus();
273 |
274 | break;
275 | }
276 | }
277 | };
278 |
279 | ctrl.toggle = function toggle (paneToToggle) {
280 | if (isDisabled || !paneToToggle) { return; }
281 |
282 | if (!$scope.allowMultiple) {
283 | ctrl.collapseAll(paneToToggle);
284 | }
285 |
286 | paneToToggle.isExpanded = !paneToToggle.isExpanded;
287 |
288 | if (paneToToggle.isExpanded) {
289 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
290 | } else {
291 | $scope.collapseCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
292 | }
293 | };
294 |
295 | ctrl.expand = function expand (paneToExpand) {
296 | if (isDisabled || !paneToExpand) { return; }
297 |
298 | if (!$scope.allowMultiple) {
299 | ctrl.collapseAll(paneToExpand);
300 | }
301 |
302 | if (!paneToExpand.isExpanded) {
303 | paneToExpand.isExpanded = true;
304 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToExpand), id: paneToExpand.id, pane: paneToExpand });
305 | }
306 | };
307 |
308 | ctrl.collapse = function collapse (paneToCollapse) {
309 | if (isDisabled || !paneToCollapse) { return; }
310 |
311 | if (paneToCollapse.isExpanded) {
312 | paneToCollapse.isExpanded = false;
313 | $scope.collapseCb({ index: ctrl.getPaneIndex(paneToCollapse), id: paneToCollapse.id, pane: paneToCollapse });
314 | }
315 | };
316 |
317 | ctrl.expandAll = function expandAll () {
318 | if (isDisabled) { return; }
319 |
320 | if ($scope.allowMultiple) {
321 | angular.forEach($scope.panes, function (iteratedPane) {
322 | ctrl.expand(iteratedPane);
323 | });
324 | } else {
325 | throw new Error('The `multiple` attribute can\'t be found');
326 | }
327 | };
328 |
329 | ctrl.collapseAll = function collapseAll (exceptionalPane) {
330 | if (isDisabled) { return; }
331 |
332 | angular.forEach($scope.panes, function (iteratedPane) {
333 | if (iteratedPane !== exceptionalPane) {
334 | ctrl.collapse(iteratedPane);
335 | }
336 | });
337 | };
338 |
339 | // API
340 | $scope.internalControl = {
341 | toggle: function toggle (indexOrId) {
342 | if (angular.isString(indexOrId)) {
343 | ctrl.toggle( ctrl.getPaneById(indexOrId) );
344 | } else {
345 | ctrl.toggle( ctrl.getPaneByIndex(indexOrId) );
346 | }
347 | },
348 | expand: function expand (indexOrId) {
349 | if (angular.isString(indexOrId)) {
350 | ctrl.expand( ctrl.getPaneById(indexOrId) );
351 | } else {
352 | ctrl.expand( ctrl.getPaneByIndex(indexOrId) );
353 | }
354 | },
355 | collapse: function collapse (indexOrId) {
356 | if (angular.isString(indexOrId)) {
357 | ctrl.collapse( ctrl.getPaneById(indexOrId) );
358 | } else {
359 | ctrl.collapse( ctrl.getPaneByIndex(indexOrId) );
360 | }
361 | },
362 | expandAll: ctrl.expandAll,
363 | collapseAll: ctrl.collapseAll,
364 | hasExpandedPane: ctrl.hasExpandedPane
365 | };
366 | }
367 | vAccordionController.$inject = ['$scope'];
368 |
369 |
370 |
371 | // vPane directive
372 | angular.module('vAccordion.directives')
373 | .directive('vPane', vPaneDirective);
374 |
375 |
376 | function vPaneDirective ($timeout, $animate, accordionConfig) {
377 | return {
378 | restrict: 'E',
379 | require: '^vAccordion',
380 | transclude: true,
381 | controller: vPaneController,
382 | scope: {
383 | isExpanded: '=?expanded',
384 | isDisabled: '=?ngDisabled',
385 | id: '@?'
386 | },
387 | link: function (scope, iElement, iAttrs, accordionCtrl, transclude) {
388 | transclude(scope.$parent.$new(), function (clone, transclusionScope) {
389 | transclusionScope.$pane = scope.internalControl;
390 | if (scope.id) { transclusionScope.$pane.id = scope.id; }
391 | iElement.append(clone);
392 | });
393 |
394 | if (!angular.isDefined(scope.isExpanded)) {
395 | scope.isExpanded = (angular.isDefined(iAttrs.expanded) && (iAttrs.expanded === ''));
396 | }
397 |
398 | if (angular.isDefined(iAttrs.disabled)) {
399 | scope.isDisabled = true;
400 | }
401 |
402 | var states = accordionConfig.states;
403 |
404 | var paneHeader = iElement.find('v-pane-header'),
405 | paneContent = iElement.find('v-pane-content'),
406 | paneInner = paneContent.find('div');
407 |
408 | var accordionId = accordionCtrl.getAccordionId();
409 |
410 | if (!paneHeader[0]) {
411 | throw new Error('The `v-pane-header` directive can\'t be found');
412 | }
413 |
414 | if (!paneContent[0]) {
415 | throw new Error('The `v-pane-content` directive can\'t be found');
416 | }
417 |
418 | scope.paneElement = iElement;
419 | scope.paneContentElement = paneContent;
420 | scope.paneInnerElement = paneInner;
421 |
422 | scope.accordionCtrl = accordionCtrl;
423 |
424 | accordionCtrl.addPane(scope);
425 |
426 | function emitEvent (eventName) {
427 | eventName = (angular.isDefined(accordionId)) ? accordionId + ':' + eventName : 'vAccordion:' + eventName;
428 | scope.$emit(eventName);
429 | }
430 |
431 | function expand () {
432 | accordionCtrl.disable();
433 |
434 | paneContent.attr('aria-hidden', 'false');
435 |
436 | paneHeader.attr({
437 | 'aria-selected': 'true',
438 | 'aria-expanded': 'true'
439 | });
440 |
441 | emitEvent('onExpand');
442 |
443 | $animate
444 | .addClass(iElement, states.expanded)
445 | .then(function () {
446 | accordionCtrl.enable();
447 | emitEvent('onExpandAnimationEnd');
448 | });
449 | }
450 |
451 | function collapse () {
452 | accordionCtrl.disable();
453 |
454 | paneContent.attr('aria-hidden', 'true');
455 |
456 | paneHeader.attr({
457 | 'aria-selected': 'false',
458 | 'aria-expanded': 'false'
459 | });
460 |
461 | emitEvent('onCollapse');
462 |
463 | $animate
464 | .removeClass(iElement, states.expanded)
465 | .then(function () {
466 | accordionCtrl.enable();
467 | emitEvent('onCollapseAnimationEnd');
468 | });
469 | }
470 |
471 | scope.$evalAsync(function () {
472 | if (scope.isExpanded) {
473 | iElement.addClass(states.expanded);
474 | paneContent
475 | .css('max-height', 'none')
476 | .attr('aria-hidden', 'false');
477 |
478 | paneHeader.attr({
479 | 'aria-selected': 'true',
480 | 'aria-expanded': 'true'
481 | });
482 | } else {
483 | paneContent
484 | .css('max-height', '0px')
485 | .attr('aria-hidden', 'true');
486 |
487 | paneHeader.attr({
488 | 'aria-selected': 'false',
489 | 'aria-expanded': 'false'
490 | });
491 | }
492 | });
493 |
494 | scope.$watch('isExpanded', function (newValue, oldValue) {
495 | if (newValue === oldValue) { return true; }
496 | if (newValue) { expand(); }
497 | else { collapse(); }
498 | });
499 | }
500 | };
501 | }
502 | vPaneDirective.$inject = ['$timeout', '$animate', 'accordionConfig'];
503 |
504 |
505 | // vPane directive controller
506 | function vPaneController ($scope) {
507 | var ctrl = this;
508 |
509 | ctrl.isExpanded = function isExpanded () {
510 | return $scope.isExpanded;
511 | };
512 |
513 | ctrl.toggle = function toggle () {
514 | if (!$scope.isAnimating && !$scope.isDisabled) {
515 | $scope.accordionCtrl.toggle($scope);
516 | }
517 | };
518 |
519 | ctrl.expand = function expand () {
520 | if (!$scope.isAnimating && !$scope.isDisabled) {
521 | $scope.accordionCtrl.expand($scope);
522 | }
523 | };
524 |
525 | ctrl.collapse = function collapse () {
526 | if (!$scope.isAnimating && !$scope.isDisabled) {
527 | $scope.accordionCtrl.collapse($scope);
528 | }
529 | };
530 |
531 | ctrl.focusPane = function focusPane () {
532 | $scope.isFocused = true;
533 | };
534 |
535 | ctrl.blurPane = function blurPane () {
536 | $scope.isFocused = false;
537 | };
538 |
539 | $scope.internalControl = {
540 | toggle: ctrl.toggle,
541 | expand: ctrl.expand,
542 | collapse: ctrl.collapse,
543 | isExpanded: ctrl.isExpanded
544 | };
545 | }
546 | vPaneController.$inject = ['$scope'];
547 |
548 |
549 |
550 | // vPaneContent directive
551 | angular.module('vAccordion.directives')
552 | .directive('vPaneContent', vPaneContentDirective);
553 |
554 |
555 | function vPaneContentDirective () {
556 | return {
557 | restrict: 'E',
558 | require: '^vPane',
559 | transclude: true,
560 | template: '',
561 | scope: {},
562 | link: function (scope, iElement, iAttrs) {
563 | iAttrs.$set('role', 'tabpanel');
564 | iAttrs.$set('aria-hidden', 'true');
565 | }
566 | };
567 | }
568 |
569 |
570 |
571 | // vPaneHeader directive
572 | angular.module('vAccordion.directives')
573 | .directive('vPaneHeader', vPaneHeaderDirective);
574 |
575 |
576 | function vPaneHeaderDirective () {
577 | return {
578 | restrict: 'E',
579 | require: ['^vPane', '^vAccordion'],
580 | transclude: true,
581 | template: '',
582 | scope: {},
583 | link: function (scope, iElement, iAttrs, ctrls) {
584 | iAttrs.$set('role', 'tab');
585 | iAttrs.$set('tabindex', '0');
586 |
587 | var paneCtrl = ctrls[0],
588 | accordionCtrl = ctrls[1];
589 |
590 | var isInactive = angular.isDefined(iAttrs.inactive);
591 |
592 | function onClick () {
593 | if (isInactive) { return false; }
594 | scope.$apply(function () { paneCtrl.toggle(); });
595 | }
596 |
597 | function onKeydown (event) {
598 | if (event.keyCode === 32 || event.keyCode === 13) {
599 | scope.$apply(function () { paneCtrl.toggle(); });
600 | event.preventDefault();
601 | } else if (event.keyCode === 39 || event.keyCode === 40) {
602 | scope.$apply(function () { accordionCtrl.focusNext(); });
603 | event.preventDefault();
604 | } else if (event.keyCode === 37 || event.keyCode === 38) {
605 | scope.$apply(function () { accordionCtrl.focusPrevious(); });
606 | event.preventDefault();
607 | }
608 | }
609 |
610 | function onFocus () {
611 | paneCtrl.focusPane();
612 | }
613 |
614 | function onBlur () {
615 | paneCtrl.blurPane();
616 | }
617 |
618 | iElement[0].onfocus = onFocus;
619 | iElement[0].onblur = onBlur;
620 | iElement.bind('click', onClick);
621 | iElement.bind('keydown', onKeydown);
622 |
623 | scope.$on('$destroy', function () {
624 | iElement.unbind('click', onClick);
625 | iElement.unbind('keydown', onKeydown);
626 | iElement[0].onfocus = null;
627 | iElement[0].onblur = null;
628 | });
629 | }
630 | };
631 | }
632 |
633 | })(angular);
--------------------------------------------------------------------------------
/dist/v-accordion.min.css:
--------------------------------------------------------------------------------
1 | /**
2 | * vAccordion - AngularJS multi-level accordion component
3 | * @version v1.6.0
4 | * @link http://lukaszwatroba.github.io/v-accordion
5 | * @author Łukasz Wątroba
6 | * @license MIT License, http://www.opensource.org/licenses/MIT
7 | */
8 |
9 | v-accordion,v-pane{display:block}v-pane.is-expanded>v-pane-content>div{display:visible}v-pane[disabled]>v-pane-header{opacity:.6;pointer-events:none}v-pane-header{display:block;position:relative;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0}v-pane-header:focus{outline:0}v-pane-header>div{display:block}v-pane-content{display:block;position:relative;overflow:hidden;max-height:0}v-pane-content>div{visibility:none}.vAccordion--default v-accordion{margin-top:20px;padding-left:20px}.vAccordion--default v-pane-content>div{padding-bottom:20px;opacity:0;-webkit-transform:translate3d(0,30px,0);transform:translate3d(0,30px,0);-webkit-transition:all .5s;transition:all .5s}.vAccordion--default v-pane{overflow:hidden}.vAccordion--default v-pane.is-expanded>v-pane-header{border-bottom-color:#2196F3}.vAccordion--default v-pane.is-expanded>v-pane-header::after{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}.vAccordion--default v-pane.is-expanded>v-pane-header::before{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.vAccordion--default v-pane.is-expanded>v-pane-content>div{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.vAccordion--default v-pane[disabled] v-pane-header::after,.vAccordion--default v-pane[disabled] v-pane-header::before{display:none}.vAccordion--default v-pane-header{padding:5px 0;margin-bottom:20px;border-bottom:2px solid #D8D8D8;-webkit-transition:all .25s;transition:all .25s}.vAccordion--default v-pane-header::after,.vAccordion--default v-pane-header::before{content:'';display:block;position:absolute;top:50%;right:0;width:10px;height:1px;background-color:#2196F3;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;will-change:transform;-webkit-transition:all .25s;transition:all .25s}.vAccordion--default v-pane-header::before{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.vAccordion--default v-pane-header:focus,.vAccordion--default v-pane-header:hover{color:#2196F3}
--------------------------------------------------------------------------------
/dist/v-accordion.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vAccordion - AngularJS multi-level accordion component
3 | * @version v1.6.0
4 | * @link http://lukaszwatroba.github.io/v-accordion
5 | * @author Łukasz Wątroba
6 | * @license MIT License, http://www.opensource.org/licenses/MIT
7 | */
8 |
9 | !function(e){"use strict";function n(n){return{restrict:"E",transclude:!0,controller:a,scope:{control:"=?",expandCb:"&?onexpand",collapseCb:"&?oncollapse",id:"@?"},link:{pre:function(n,a,i){n.allowMultiple=e.isDefined(i.multiple)&&(""===i.multiple||"true"===i.multiple)},post:function(a,i,t,o,d){function r(){var n=["toggle","expand","collapse","expandAll","collapseAll","hasExpandedPane"];e.forEach(n,function(e){if(a.control[e])throw new Error("The `"+e+"` method can not be overwritten")})}if(d(a.$parent.$new(),function(e,n){n.$accordion=a.internalControl,a.id&&(n.$accordion.id=a.id),i.append(e)}),t.$set("role","tablist"),a.allowMultiple&&t.$set("aria-multiselectable","true"),e.isDefined(a.control)){r();var c=e.extend({},a.internalControl,a.control);a.control=a.internalControl=c}else a.control=a.internalControl;n(function(){var n=e.isDefined(o.getAccordionId())?o.getAccordionId()+":onReady":"vAccordion:onReady";a.$emit(n)},0)}}}}function a(n){var a=this,i=!1;n.panes=[],n.expandCb=e.isFunction(n.expandCb)?n.expandCb:e.noop,n.collapseCb=e.isFunction(n.collapseCb)?n.collapseCb:e.noop,a.hasExpandedPane=function(){for(var e=!1,a=0,i=n.panes.length;i>a;a++){var t=n.panes[a];if(t.isExpanded){e=!0;break}}return e},a.getPaneByIndex=function(a){var i;return e.forEach(n.panes,function(n){n.$parent&&e.isDefined(n.$parent.$index)&&n.$parent.$index===a&&(i=n)}),i?i:n.panes[a]},a.getPaneIndex=function(a){var i;return e.forEach(n.panes,function(n){n.$parent&&e.isDefined(n.$parent.$index)&&n===a&&(i=n.$parent.$index)}),e.isDefined(i)?i:n.panes.indexOf(a)},a.getPaneById=function(a){var i;return e.forEach(n.panes,function(e){e.id&&e.id===a&&(i=e)}),i},a.getPaneId=function(e){return e.id},a.getAccordionId=function(){return n.id},a.disable=function(){i=!0},a.enable=function(){i=!1},a.addPane=function(e){if(!n.allowMultiple&&a.hasExpandedPane()&&e.isExpanded)throw new Error("The `multiple` attribute can't be found");n.panes.push(e),e.isExpanded&&n.expandCb({index:a.getPaneIndex(e),id:e.id,pane:e})},a.focusNext=function(){for(var e=n.panes.length,a=0;e>a;a++){var i=n.panes[a];if(i.isFocused){var t=a+1;t>n.panes.length-1&&(t=0);var o=n.panes[t];o.paneElement.find("v-pane-header")[0].focus();break}}},a.focusPrevious=function(){for(var e=n.panes.length,a=0;e>a;a++){var i=n.panes[a];if(i.isFocused){var t=a-1;0>t&&(t=n.panes.length-1);var o=n.panes[t];o.paneElement.find("v-pane-header")[0].focus();break}}},a.toggle=function(e){!i&&e&&(n.allowMultiple||a.collapseAll(e),e.isExpanded=!e.isExpanded,e.isExpanded?n.expandCb({index:a.getPaneIndex(e),id:e.id,pane:e}):n.collapseCb({index:a.getPaneIndex(e),id:e.id,pane:e}))},a.expand=function(e){!i&&e&&(n.allowMultiple||a.collapseAll(e),e.isExpanded||(e.isExpanded=!0,n.expandCb({index:a.getPaneIndex(e),id:e.id,pane:e})))},a.collapse=function(e){!i&&e&&e.isExpanded&&(e.isExpanded=!1,n.collapseCb({index:a.getPaneIndex(e),id:e.id,pane:e}))},a.expandAll=function(){if(!i){if(!n.allowMultiple)throw new Error("The `multiple` attribute can't be found");e.forEach(n.panes,function(e){a.expand(e)})}},a.collapseAll=function(t){i||e.forEach(n.panes,function(e){e!==t&&a.collapse(e)})},n.internalControl={toggle:function(n){e.isString(n)?a.toggle(a.getPaneById(n)):a.toggle(a.getPaneByIndex(n))},expand:function(n){e.isString(n)?a.expand(a.getPaneById(n)):a.expand(a.getPaneByIndex(n))},collapse:function(n){e.isString(n)?a.collapse(a.getPaneById(n)):a.collapse(a.getPaneByIndex(n))},expandAll:a.expandAll,collapseAll:a.collapseAll,hasExpandedPane:a.hasExpandedPane}}function i(n,a,i){return{restrict:"E",require:"^vAccordion",transclude:!0,controller:t,scope:{isExpanded:"=?expanded",isDisabled:"=?ngDisabled",id:"@?"},link:function(n,t,o,d,r){function c(a){a=e.isDefined(v)?v+":"+a:"vAccordion:"+a,n.$emit(a)}function l(){d.disable(),f.attr("aria-hidden","false"),u.attr({"aria-selected":"true","aria-expanded":"true"}),c("onExpand"),a.addClass(t,p.expanded).then(function(){d.enable(),c("onExpandAnimationEnd")})}function s(){d.disable(),f.attr("aria-hidden","true"),u.attr({"aria-selected":"false","aria-expanded":"false"}),c("onCollapse"),a.removeClass(t,p.expanded).then(function(){d.enable(),c("onCollapseAnimationEnd")})}r(n.$parent.$new(),function(e,a){a.$pane=n.internalControl,n.id&&(a.$pane.id=n.id),t.append(e)}),e.isDefined(n.isExpanded)||(n.isExpanded=e.isDefined(o.expanded)&&""===o.expanded),e.isDefined(o.disabled)&&(n.isDisabled=!0);var p=i.states,u=t.find("v-pane-header"),f=t.find("v-pane-content"),x=f.find("div"),v=d.getAccordionId();if(!u[0])throw new Error("The `v-pane-header` directive can't be found");if(!f[0])throw new Error("The `v-pane-content` directive can't be found");n.paneElement=t,n.paneContentElement=f,n.paneInnerElement=x,n.accordionCtrl=d,d.addPane(n),n.$evalAsync(function(){n.isExpanded?(t.addClass(p.expanded),f.css("max-height","none").attr("aria-hidden","false"),u.attr({"aria-selected":"true","aria-expanded":"true"})):(f.css("max-height","0px").attr("aria-hidden","true"),u.attr({"aria-selected":"false","aria-expanded":"false"}))}),n.$watch("isExpanded",function(e,n){return e===n?!0:(e?l():s(),void 0)})}}}function t(e){var n=this;n.isExpanded=function(){return e.isExpanded},n.toggle=function(){e.isAnimating||e.isDisabled||e.accordionCtrl.toggle(e)},n.expand=function(){e.isAnimating||e.isDisabled||e.accordionCtrl.expand(e)},n.collapse=function(){e.isAnimating||e.isDisabled||e.accordionCtrl.collapse(e)},n.focusPane=function(){e.isFocused=!0},n.blurPane=function(){e.isFocused=!1},e.internalControl={toggle:n.toggle,expand:n.expand,collapse:n.collapse,isExpanded:n.isExpanded}}function o(){return{restrict:"E",require:"^vPane",transclude:!0,template:"",scope:{},link:function(e,n,a){a.$set("role","tabpanel"),a.$set("aria-hidden","true")}}}function d(){return{restrict:"E",require:["^vPane","^vAccordion"],transclude:!0,template:"",scope:{},link:function(n,a,i,t){function o(){return p?!1:(n.$apply(function(){l.toggle()}),void 0)}function d(e){32===e.keyCode||13===e.keyCode?(n.$apply(function(){l.toggle()}),e.preventDefault()):39===e.keyCode||40===e.keyCode?(n.$apply(function(){s.focusNext()}),e.preventDefault()):(37===e.keyCode||38===e.keyCode)&&(n.$apply(function(){s.focusPrevious()}),e.preventDefault())}function r(){l.focusPane()}function c(){l.blurPane()}i.$set("role","tab"),i.$set("tabindex","0");var l=t[0],s=t[1],p=e.isDefined(i.inactive);a[0].onfocus=r,a[0].onblur=c,a.bind("click",o),a.bind("keydown",d),n.$on("$destroy",function(){a.unbind("click",o),a.unbind("keydown",d),a[0].onfocus=null,a[0].onblur=null})}}}e.module("vAccordion.config",[]).constant("accordionConfig",{states:{expanded:"is-expanded"},expandAnimationDuration:.5}).animation(".is-expanded",["$animateCss","accordionConfig",function(n,a){return{addClass:function(i,t,o){var d=e.element(i[0].querySelector("v-pane-content")),r=e.element(d[0].querySelector("div")),c=r[0].offsetHeight,l=n(d,{easing:"ease",from:{maxHeight:"0px"},to:{maxHeight:c+"px"},duration:a.expandAnimationDuration});return l.start().done(function(){d.css("max-height","none"),o()}),function(e){e&&d.css("max-height","none")}},removeClass:function(i,t,o){var d=e.element(i[0].querySelector("v-pane-content")),r=e.element(d[0].querySelector("div")),c=r[0].offsetHeight,l=n(d,{easing:"ease",from:{maxHeight:c+"px"},to:{maxHeight:"0px"},duration:a.expandAnimationDuration});return l.start().done(o),function(e){e&&d.css("max-height","0px")}}}}]),e.module("vAccordion.directives",[]),e.module("vAccordion",["vAccordion.config","vAccordion.directives"]),e.module("vAccordion.directives").directive("vAccordion",n),n.$inject=["$timeout"],a.$inject=["$scope"],e.module("vAccordion.directives").directive("vPane",i),i.$inject=["$timeout","$animate","accordionConfig"],t.$inject=["$scope"],e.module("vAccordion.directives").directive("vPaneContent",o),e.module("vAccordion.directives").directive("vPaneHeader",d)}(angular);
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var pkg = require('./package.json');
2 |
3 | var banner = '/**\n' +
4 | ' * <%= pkg.description %>\n' +
5 | ' * @version v<%= pkg.version %>\n' +
6 | ' * @link <%= pkg.homepage %>\n' +
7 | ' * @author <%= pkg.author %>\n' +
8 | ' * @license MIT License, http://www.opensource.org/licenses/MIT\n' +
9 | ' */\n\n';
10 |
11 | var gulp = require('gulp'),
12 | karma = require('karma').server,
13 | jshint = require('gulp-jshint'),
14 | concat = require('gulp-concat'),
15 | uglify = require('gulp-uglify'),
16 | rename = require('gulp-rename'),
17 | sass = require('gulp-sass');
18 | autoprefixer = require('gulp-autoprefixer'),
19 | minifycss = require('gulp-minify-css'),
20 | header = require('gulp-header');
21 |
22 | gulp.task('scripts', function() {
23 | gulp.src([
24 | 'src/vAccordion/vAccordion.prefix',
25 | 'src/vAccordion/*.js',
26 | 'src/vAccordion/directives/*.js',
27 | 'src/vAccordion/services/*.js',
28 | 'src/vAccordion/vAccordion.suffix'
29 | ])
30 | .pipe(concat('v-accordion.js'))
31 | .pipe(header(banner, { pkg : pkg } ))
32 | .pipe(gulp.dest('./dist/'))
33 | .pipe(uglify())
34 | .pipe(rename('v-accordion.min.js'))
35 | .pipe(header(banner, { pkg : pkg } ))
36 | .pipe(gulp.dest('./dist'))
37 | });
38 |
39 | gulp.task('styles', function() {
40 | return gulp.src('src/vAccordion/styles/vAccordion.scss')
41 | .pipe(sass({style: 'expanded'}))
42 | .pipe(rename({basename: 'v-accordion'} ))
43 | .pipe(autoprefixer('last 2 version'))
44 | .pipe(header(banner, { pkg : pkg } ))
45 | .pipe(gulp.dest('dist/'))
46 | .pipe(rename({suffix: '.min'} ))
47 | .pipe(minifycss())
48 | .pipe(header(banner, { pkg : pkg } ))
49 | .pipe(gulp.dest('dist/'))
50 | });
51 |
52 | gulp.task('test', function (done) {
53 | karma.start({
54 | configFile: __dirname + '/karma.conf.js',
55 | singleRun: true
56 | }, done);
57 | });
58 |
59 | gulp.task('lint-src', function() {
60 | return gulp.src([
61 | 'src/vAccordion/**/*.js',
62 | ])
63 | .pipe(jshint('.jshintrc'))
64 | .pipe(jshint.reporter('jshint-stylish'));
65 | });
66 |
67 | gulp.task('lint-tests', function() {
68 | return gulp.src([
69 | 'test/**/*.spec.js'
70 | ])
71 | .pipe(jshint('.jshintrc'))
72 | .pipe(jshint.reporter('jshint-stylish'));
73 | });
74 |
75 | gulp.task('default', ['lint-src', 'test', 'scripts', 'styles']);
76 |
77 | gulp.task('watch', function() {
78 | gulp.watch('src/vAccordion/**/*.js', ['lint-src', 'scripts', 'test']);
79 | gulp.watch('test/**/*.spec.js', ['lint-tests', 'test']);
80 |
81 | gulp.watch('src/vAccordion/styles/**/*.scss', ['styles']);
82 | });
83 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | vAccordion
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
vAccordion
37 |
38 |
39 | AngularJS multi-level accordion component.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
Works with (or without) ng-repeat
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 | {{ ::pane.content }}
58 |
59 |
60 |
61 |
64 |
65 | {{ ::subpane.content }}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
83 |
84 |
85 |
86 |
Allows multiple sections open at once
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 | {{ ::pane.content }}
97 |
98 |
99 |
100 |
103 |
104 | {{ ::subpane.content }}
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
122 |
123 |
124 |
125 |
126 |
127 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('./dist/v-accordion');
2 | module.exports = 'vAccordion';
3 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine', 'jquery-1.8.3'],
14 |
15 | plugins: [
16 | 'karma-jasmine',
17 | 'karma-chrome-launcher',
18 | 'karma-phantomjs-launcher',
19 | 'karma-jquery'
20 | ],
21 |
22 | // list of files / patterns to load in the browser
23 | files: [
24 | 'bower/angular/angular.js',
25 | 'bower/angular-mocks/angular-mocks.js',
26 | 'bower/angular-animate/angular-animate.js',
27 | 'src/vAccordion/*.js',
28 | 'src/vAccordion/directives/*.js',
29 | 'test/unit/**/*.js'
30 | ],
31 |
32 |
33 | // list of files to exclude
34 | exclude: [
35 | ],
36 |
37 |
38 | // preprocess matching files before serving them to the browser
39 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
40 | preprocessors: {
41 | },
42 |
43 |
44 | // test results reporter to use
45 | // possible values: 'dots', 'progress'
46 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
47 | reporters: ['progress'],
48 |
49 |
50 | // web server port
51 | port: 9876,
52 |
53 |
54 | // enable / disable colors in the output (reporters and logs)
55 | colors: true,
56 |
57 |
58 | // level of logging
59 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
60 | logLevel: config.LOG_INFO,
61 |
62 |
63 | // enable / disable watching file and executing tests whenever any file changes
64 | autoWatch: true,
65 |
66 |
67 | // start these browsers
68 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
69 | browsers: ['Chrome'],
70 |
71 |
72 | // Continuous Integration mode
73 | // if true, Karma captures browsers, runs the tests and exits
74 | singleRun: false
75 | });
76 | };
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v-accordion",
3 | "description": "vAccordion - AngularJS multi-level accordion component",
4 | "version": "1.6.0",
5 | "author": "Łukasz Wątroba ",
6 | "license": "MIT",
7 | "keywords": [
8 | "angular",
9 | "accordion",
10 | "multi-level",
11 | "component",
12 | "directive",
13 | "module"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/LukaszWatroba/v-accordion.git"
18 | },
19 | "main": "index.js",
20 | "bugs": {
21 | "url": "https://github.com/LukaszWatroba/v-accordion/issues"
22 | },
23 | "homepage": "http://lukaszwatroba.github.io/v-accordion",
24 | "devDependencies": {
25 | "chai": "^1.9.1",
26 | "chai-jquery": "^1.2.3",
27 | "gulp": "^3.8.11",
28 | "gulp-autoprefixer": "^2.0.0",
29 | "gulp-concat": "^2.3.4",
30 | "gulp-header": "^1.2.2",
31 | "gulp-jshint": "^1.9.0",
32 | "gulp-minify-css": "^0.3.11",
33 | "gulp-rename": "^1.2.0",
34 | "gulp-sass": "^2.3.1",
35 | "gulp-uglify": "^0.3.2",
36 | "jshint-stylish": "^1.0.0",
37 | "karma": "^0.12.22",
38 | "karma-chai": "^0.1.0",
39 | "karma-chai-jquery": "^1.0.0",
40 | "karma-chrome-launcher": "^0.1.4",
41 | "karma-jasmine": "^0.1.5",
42 | "karma-jquery": "^0.1.0",
43 | "karma-mocha": "^0.1.8",
44 | "karma-phantomjs-launcher": "^0.1.4",
45 | "karma-sinon-chai": "^0.2.0",
46 | "mocha": "^1.21.4",
47 | "sinon": "^1.10.3",
48 | "sinon-chai": "^2.5.0"
49 | },
50 | "engines": {
51 | "node": ">=0.8.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/vAccordion/directives/vAccordion.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // vAccordion directive
4 | angular.module('vAccordion.directives')
5 | .directive('vAccordion', vAccordionDirective);
6 |
7 |
8 | function vAccordionDirective ($timeout) {
9 | return {
10 | restrict: 'E',
11 | transclude: true,
12 | controller: vAccordionController,
13 | scope: {
14 | control: '=?',
15 | expandCb: '&?onexpand',
16 | collapseCb: '&?oncollapse',
17 | id: '@?'
18 | },
19 | link: {
20 | pre: function (scope, iElement, iAttrs) {
21 | scope.allowMultiple = (angular.isDefined(iAttrs.multiple) && (iAttrs.multiple === '' || iAttrs.multiple === 'true'));
22 | },
23 | post: function (scope, iElement, iAttrs, ctrl, transclude) {
24 | transclude(scope.$parent.$new(), function (clone, transclusionScope) {
25 | transclusionScope.$accordion = scope.internalControl;
26 | if (scope.id) { transclusionScope.$accordion.id = scope.id; }
27 | iElement.append(clone);
28 | });
29 |
30 | iAttrs.$set('role', 'tablist');
31 |
32 | if (scope.allowMultiple) {
33 | iAttrs.$set('aria-multiselectable', 'true');
34 | }
35 |
36 | if (angular.isDefined(scope.control)) {
37 | checkCustomControlAPIMethods();
38 |
39 | var mergedControl = angular.extend({}, scope.internalControl, scope.control);
40 | scope.control = scope.internalControl = mergedControl;
41 | }
42 | else {
43 | scope.control = scope.internalControl;
44 | }
45 |
46 | function checkCustomControlAPIMethods () {
47 | var protectedApiMethods = ['toggle', 'expand', 'collapse', 'expandAll', 'collapseAll', 'hasExpandedPane'];
48 |
49 | angular.forEach(protectedApiMethods, function (iteratedMethodName) {
50 | if (scope.control[iteratedMethodName]) {
51 | throw new Error('The `' + iteratedMethodName + '` method can not be overwritten');
52 | }
53 | });
54 | }
55 |
56 | $timeout(function () {
57 | var eventName = (angular.isDefined(ctrl.getAccordionId())) ? ctrl.getAccordionId() + ':onReady' : 'vAccordion:onReady';
58 | scope.$emit(eventName);
59 | }, 0);
60 | }
61 | }
62 | };
63 | }
64 | vAccordionDirective.$inject = ['$timeout'];
65 |
66 |
67 | // vAccordion directive controller
68 | function vAccordionController ($scope) {
69 | var ctrl = this;
70 | var isDisabled = false;
71 |
72 | $scope.panes = [];
73 |
74 | $scope.expandCb = (angular.isFunction($scope.expandCb)) ? $scope.expandCb : angular.noop;
75 | $scope.collapseCb = (angular.isFunction($scope.collapseCb)) ? $scope.collapseCb : angular.noop;
76 |
77 | ctrl.hasExpandedPane = function hasExpandedPane () {
78 | var bool = false;
79 |
80 | for (var i = 0, length = $scope.panes.length; i < length; i++) {
81 | var iteratedPane = $scope.panes[i];
82 |
83 | if (iteratedPane.isExpanded) {
84 | bool = true;
85 | break;
86 | }
87 | }
88 |
89 | return bool;
90 | };
91 |
92 | ctrl.getPaneByIndex = function getPaneByIndex (index) {
93 | var thePane;
94 |
95 | angular.forEach($scope.panes, function (iteratedPane) {
96 | if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane.$parent.$index === index) {
97 | thePane = iteratedPane;
98 | }
99 | });
100 |
101 | return (thePane) ? thePane : $scope.panes[index];
102 | };
103 |
104 | ctrl.getPaneIndex = function getPaneIndex (pane) {
105 | var theIndex;
106 |
107 | angular.forEach($scope.panes, function (iteratedPane) {
108 | if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane === pane) {
109 | theIndex = iteratedPane.$parent.$index;
110 | }
111 | });
112 |
113 | return (angular.isDefined(theIndex)) ? theIndex : $scope.panes.indexOf(pane);
114 | };
115 |
116 | ctrl.getPaneById = function getPaneById (id) {
117 | var thePane;
118 |
119 | angular.forEach($scope.panes, function (iteratedPane) {
120 | if (iteratedPane.id && iteratedPane.id === id) {
121 | thePane = iteratedPane;
122 | }
123 | });
124 |
125 | return thePane;
126 | };
127 |
128 | ctrl.getPaneId = function getPaneId (pane) {
129 | return pane.id;
130 | };
131 |
132 | ctrl.getAccordionId = function getAccordionId () {
133 | return $scope.id;
134 | };
135 |
136 |
137 | ctrl.disable = function disable () {
138 | isDisabled = true;
139 | };
140 |
141 | ctrl.enable = function enable () {
142 | isDisabled = false;
143 | };
144 |
145 | ctrl.addPane = function addPane (paneToAdd) {
146 | if (!$scope.allowMultiple) {
147 | if (ctrl.hasExpandedPane() && paneToAdd.isExpanded) {
148 | throw new Error('The `multiple` attribute can\'t be found');
149 | }
150 | }
151 |
152 | $scope.panes.push(paneToAdd);
153 |
154 | if (paneToAdd.isExpanded) {
155 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToAdd), id: paneToAdd.id, pane: paneToAdd });
156 | }
157 | };
158 |
159 | ctrl.focusNext = function focusNext () {
160 | var length = $scope.panes.length;
161 |
162 | for (var i = 0; i < length; i++) {
163 | var iteratedPane = $scope.panes[i];
164 |
165 | if (iteratedPane.isFocused) {
166 | var paneToFocusIndex = i + 1;
167 |
168 | if (paneToFocusIndex > $scope.panes.length - 1) {
169 | paneToFocusIndex = 0;
170 | }
171 |
172 | var paneToFocus = $scope.panes[paneToFocusIndex];
173 | paneToFocus.paneElement.find('v-pane-header')[0].focus();
174 |
175 | break;
176 | }
177 | }
178 | };
179 |
180 | ctrl.focusPrevious = function focusPrevious () {
181 | var length = $scope.panes.length;
182 |
183 | for (var i = 0; i < length; i++) {
184 | var iteratedPane = $scope.panes[i];
185 |
186 | if (iteratedPane.isFocused) {
187 | var paneToFocusIndex = i - 1;
188 |
189 | if (paneToFocusIndex < 0) {
190 | paneToFocusIndex = $scope.panes.length - 1;
191 | }
192 |
193 | var paneToFocus = $scope.panes[paneToFocusIndex];
194 | paneToFocus.paneElement.find('v-pane-header')[0].focus();
195 |
196 | break;
197 | }
198 | }
199 | };
200 |
201 | ctrl.toggle = function toggle (paneToToggle) {
202 | if (isDisabled || !paneToToggle) { return; }
203 |
204 | if (!$scope.allowMultiple) {
205 | ctrl.collapseAll(paneToToggle);
206 | }
207 |
208 | paneToToggle.isExpanded = !paneToToggle.isExpanded;
209 |
210 | if (paneToToggle.isExpanded) {
211 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
212 | } else {
213 | $scope.collapseCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
214 | }
215 | };
216 |
217 | ctrl.expand = function expand (paneToExpand) {
218 | if (isDisabled || !paneToExpand) { return; }
219 |
220 | if (!$scope.allowMultiple) {
221 | ctrl.collapseAll(paneToExpand);
222 | }
223 |
224 | if (!paneToExpand.isExpanded) {
225 | paneToExpand.isExpanded = true;
226 | $scope.expandCb({ index: ctrl.getPaneIndex(paneToExpand), id: paneToExpand.id, pane: paneToExpand });
227 | }
228 | };
229 |
230 | ctrl.collapse = function collapse (paneToCollapse) {
231 | if (isDisabled || !paneToCollapse) { return; }
232 |
233 | if (paneToCollapse.isExpanded) {
234 | paneToCollapse.isExpanded = false;
235 | $scope.collapseCb({ index: ctrl.getPaneIndex(paneToCollapse), id: paneToCollapse.id, pane: paneToCollapse });
236 | }
237 | };
238 |
239 | ctrl.expandAll = function expandAll () {
240 | if (isDisabled) { return; }
241 |
242 | if ($scope.allowMultiple) {
243 | angular.forEach($scope.panes, function (iteratedPane) {
244 | ctrl.expand(iteratedPane);
245 | });
246 | } else {
247 | throw new Error('The `multiple` attribute can\'t be found');
248 | }
249 | };
250 |
251 | ctrl.collapseAll = function collapseAll (exceptionalPane) {
252 | if (isDisabled) { return; }
253 |
254 | angular.forEach($scope.panes, function (iteratedPane) {
255 | if (iteratedPane !== exceptionalPane) {
256 | ctrl.collapse(iteratedPane);
257 | }
258 | });
259 | };
260 |
261 | // API
262 | $scope.internalControl = {
263 | toggle: function toggle (indexOrId) {
264 | if (angular.isString(indexOrId)) {
265 | ctrl.toggle( ctrl.getPaneById(indexOrId) );
266 | } else {
267 | ctrl.toggle( ctrl.getPaneByIndex(indexOrId) );
268 | }
269 | },
270 | expand: function expand (indexOrId) {
271 | if (angular.isString(indexOrId)) {
272 | ctrl.expand( ctrl.getPaneById(indexOrId) );
273 | } else {
274 | ctrl.expand( ctrl.getPaneByIndex(indexOrId) );
275 | }
276 | },
277 | collapse: function collapse (indexOrId) {
278 | if (angular.isString(indexOrId)) {
279 | ctrl.collapse( ctrl.getPaneById(indexOrId) );
280 | } else {
281 | ctrl.collapse( ctrl.getPaneByIndex(indexOrId) );
282 | }
283 | },
284 | expandAll: ctrl.expandAll,
285 | collapseAll: ctrl.collapseAll,
286 | hasExpandedPane: ctrl.hasExpandedPane
287 | };
288 | }
289 | vAccordionController.$inject = ['$scope'];
290 |
--------------------------------------------------------------------------------
/src/vAccordion/directives/vPane.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // vPane directive
4 | angular.module('vAccordion.directives')
5 | .directive('vPane', vPaneDirective);
6 |
7 |
8 | function vPaneDirective ($timeout, $animate, accordionConfig) {
9 | return {
10 | restrict: 'E',
11 | require: '^vAccordion',
12 | transclude: true,
13 | controller: vPaneController,
14 | scope: {
15 | isExpanded: '=?expanded',
16 | isDisabled: '=?ngDisabled',
17 | id: '@?'
18 | },
19 | link: function (scope, iElement, iAttrs, accordionCtrl, transclude) {
20 | transclude(scope.$parent.$new(), function (clone, transclusionScope) {
21 | transclusionScope.$pane = scope.internalControl;
22 | if (scope.id) { transclusionScope.$pane.id = scope.id; }
23 | iElement.append(clone);
24 | });
25 |
26 | if (!angular.isDefined(scope.isExpanded)) {
27 | scope.isExpanded = (angular.isDefined(iAttrs.expanded) && (iAttrs.expanded === ''));
28 | }
29 |
30 | if (angular.isDefined(iAttrs.disabled)) {
31 | scope.isDisabled = true;
32 | }
33 |
34 | var states = accordionConfig.states;
35 |
36 | var paneHeader = iElement.find('v-pane-header'),
37 | paneContent = iElement.find('v-pane-content'),
38 | paneInner = paneContent.find('div');
39 |
40 | var accordionId = accordionCtrl.getAccordionId();
41 |
42 | if (!paneHeader[0]) {
43 | throw new Error('The `v-pane-header` directive can\'t be found');
44 | }
45 |
46 | if (!paneContent[0]) {
47 | throw new Error('The `v-pane-content` directive can\'t be found');
48 | }
49 |
50 | scope.paneElement = iElement;
51 | scope.paneContentElement = paneContent;
52 | scope.paneInnerElement = paneInner;
53 |
54 | scope.accordionCtrl = accordionCtrl;
55 |
56 | accordionCtrl.addPane(scope);
57 |
58 | function emitEvent (eventName) {
59 | eventName = (angular.isDefined(accordionId)) ? accordionId + ':' + eventName : 'vAccordion:' + eventName;
60 | scope.$emit(eventName);
61 | }
62 |
63 | function expand () {
64 | accordionCtrl.disable();
65 |
66 | paneContent.attr('aria-hidden', 'false');
67 |
68 | paneHeader.attr({
69 | 'aria-selected': 'true',
70 | 'aria-expanded': 'true'
71 | });
72 |
73 | emitEvent('onExpand');
74 |
75 | $animate
76 | .addClass(iElement, states.expanded)
77 | .then(function () {
78 | accordionCtrl.enable();
79 | emitEvent('onExpandAnimationEnd');
80 | });
81 | }
82 |
83 | function collapse () {
84 | accordionCtrl.disable();
85 |
86 | paneContent.attr('aria-hidden', 'true');
87 |
88 | paneHeader.attr({
89 | 'aria-selected': 'false',
90 | 'aria-expanded': 'false'
91 | });
92 |
93 | emitEvent('onCollapse');
94 |
95 | $animate
96 | .removeClass(iElement, states.expanded)
97 | .then(function () {
98 | accordionCtrl.enable();
99 | emitEvent('onCollapseAnimationEnd');
100 | });
101 | }
102 |
103 | scope.$evalAsync(function () {
104 | if (scope.isExpanded) {
105 | iElement.addClass(states.expanded);
106 | paneContent
107 | .css('max-height', 'none')
108 | .attr('aria-hidden', 'false');
109 |
110 | paneHeader.attr({
111 | 'aria-selected': 'true',
112 | 'aria-expanded': 'true'
113 | });
114 | } else {
115 | paneContent
116 | .css('max-height', '0px')
117 | .attr('aria-hidden', 'true');
118 |
119 | paneHeader.attr({
120 | 'aria-selected': 'false',
121 | 'aria-expanded': 'false'
122 | });
123 | }
124 | });
125 |
126 | scope.$watch('isExpanded', function (newValue, oldValue) {
127 | if (newValue === oldValue) { return true; }
128 | if (newValue) { expand(); }
129 | else { collapse(); }
130 | });
131 | }
132 | };
133 | }
134 | vPaneDirective.$inject = ['$timeout', '$animate', 'accordionConfig'];
135 |
136 |
137 | // vPane directive controller
138 | function vPaneController ($scope) {
139 | var ctrl = this;
140 |
141 | ctrl.isExpanded = function isExpanded () {
142 | return $scope.isExpanded;
143 | };
144 |
145 | ctrl.toggle = function toggle () {
146 | if (!$scope.isAnimating && !$scope.isDisabled) {
147 | $scope.accordionCtrl.toggle($scope);
148 | }
149 | };
150 |
151 | ctrl.expand = function expand () {
152 | if (!$scope.isAnimating && !$scope.isDisabled) {
153 | $scope.accordionCtrl.expand($scope);
154 | }
155 | };
156 |
157 | ctrl.collapse = function collapse () {
158 | if (!$scope.isAnimating && !$scope.isDisabled) {
159 | $scope.accordionCtrl.collapse($scope);
160 | }
161 | };
162 |
163 | ctrl.focusPane = function focusPane () {
164 | $scope.isFocused = true;
165 | };
166 |
167 | ctrl.blurPane = function blurPane () {
168 | $scope.isFocused = false;
169 | };
170 |
171 | $scope.internalControl = {
172 | toggle: ctrl.toggle,
173 | expand: ctrl.expand,
174 | collapse: ctrl.collapse,
175 | isExpanded: ctrl.isExpanded
176 | };
177 | }
178 | vPaneController.$inject = ['$scope'];
179 |
--------------------------------------------------------------------------------
/src/vAccordion/directives/vPaneContent.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // vPaneContent directive
4 | angular.module('vAccordion.directives')
5 | .directive('vPaneContent', vPaneContentDirective);
6 |
7 |
8 | function vPaneContentDirective () {
9 | return {
10 | restrict: 'E',
11 | require: '^vPane',
12 | transclude: true,
13 | template: '',
14 | scope: {},
15 | link: function (scope, iElement, iAttrs) {
16 | iAttrs.$set('role', 'tabpanel');
17 | iAttrs.$set('aria-hidden', 'true');
18 | }
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/src/vAccordion/directives/vPaneHeader.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // vPaneHeader directive
4 | angular.module('vAccordion.directives')
5 | .directive('vPaneHeader', vPaneHeaderDirective);
6 |
7 |
8 | function vPaneHeaderDirective () {
9 | return {
10 | restrict: 'E',
11 | require: ['^vPane', '^vAccordion'],
12 | transclude: true,
13 | template: '',
14 | scope: {},
15 | link: function (scope, iElement, iAttrs, ctrls) {
16 | iAttrs.$set('role', 'tab');
17 | iAttrs.$set('tabindex', '0');
18 |
19 | var paneCtrl = ctrls[0],
20 | accordionCtrl = ctrls[1];
21 |
22 | var isInactive = angular.isDefined(iAttrs.inactive);
23 |
24 | function onClick () {
25 | if (isInactive) { return false; }
26 | scope.$apply(function () { paneCtrl.toggle(); });
27 | }
28 |
29 | function onKeydown (event) {
30 | if (event.keyCode === 32 || event.keyCode === 13) {
31 | scope.$apply(function () { paneCtrl.toggle(); });
32 | event.preventDefault();
33 | } else if (event.keyCode === 39 || event.keyCode === 40) {
34 | scope.$apply(function () { accordionCtrl.focusNext(); });
35 | event.preventDefault();
36 | } else if (event.keyCode === 37 || event.keyCode === 38) {
37 | scope.$apply(function () { accordionCtrl.focusPrevious(); });
38 | event.preventDefault();
39 | }
40 | }
41 |
42 | function onFocus () {
43 | paneCtrl.focusPane();
44 | }
45 |
46 | function onBlur () {
47 | paneCtrl.blurPane();
48 | }
49 |
50 | iElement[0].onfocus = onFocus;
51 | iElement[0].onblur = onBlur;
52 | iElement.bind('click', onClick);
53 | iElement.bind('keydown', onKeydown);
54 |
55 | scope.$on('$destroy', function () {
56 | iElement.unbind('click', onClick);
57 | iElement.unbind('keydown', onKeydown);
58 | iElement[0].onfocus = null;
59 | iElement[0].onblur = null;
60 | });
61 | }
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/src/vAccordion/styles/_base.scss:
--------------------------------------------------------------------------------
1 | /* Base styles
2 | ***************************************/
3 |
4 |
5 | v-accordion {
6 | display: block;
7 | }
8 |
9 | v-pane {
10 | display: block;
11 |
12 | &.is-expanded {
13 | > v-pane-content {
14 | > div {
15 | display: visible;
16 | }
17 | }
18 | }
19 |
20 | &[disabled] > v-pane-header {
21 | opacity: $v-pane-disabled-opacity;
22 | pointer-events: none;
23 | }
24 | }
25 |
26 | v-pane-header {
27 | display: block;
28 | position: relative;
29 | cursor: pointer;
30 | user-select: none;
31 | outline: none;
32 |
33 | &:focus {
34 | outline: none;
35 | }
36 |
37 | > div {
38 | display: block;
39 | }
40 | }
41 |
42 | v-pane-content {
43 | display: block;
44 | position: relative;
45 | overflow: hidden;
46 | max-height: 0px;
47 |
48 | > div {
49 | visibility: none;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/vAccordion/styles/_settings.scss:
--------------------------------------------------------------------------------
1 | // -------------------------------------
2 | // Settings
3 | // -------------------------------------
4 |
5 |
6 | $v-accordion-default-theme: true !default;
7 |
8 |
9 | // Accordion
10 | // -------------------------------------
11 |
12 | $v-accordion-spacing: 20px !default;
13 |
14 | $v-pane-border-color: #D8D8D8 !default;
15 | $v-pane-expanded-border-color: #2196F3 !default;
16 | $v-pane-icon-color: #2196F3 !default;
17 | $v-pane-hover-color: #2196F3 !default;
18 | $v-pane-disabled-opacity: 0.6 !default;
19 |
20 | $v-pane-expand-animation-duration: 0.5s !default;
21 | $v-pane-hover-animation-duration: 0.25s !default;
22 |
--------------------------------------------------------------------------------
/src/vAccordion/styles/_theme.scss:
--------------------------------------------------------------------------------
1 | @if $v-accordion-default-theme {
2 | /* Theme: default
3 | ***************************************/
4 |
5 |
6 | .vAccordion--default {
7 |
8 | v-accordion {
9 | margin-top: $v-accordion-spacing;
10 | padding-left: $v-accordion-spacing;
11 | }
12 |
13 | v-pane-content {
14 | > div {
15 | padding-bottom: $v-accordion-spacing;
16 | opacity: 0;
17 | transform: translate3d(0, 30px, 0);
18 | transition: all $v-pane-expand-animation-duration;
19 | }
20 | }
21 |
22 | v-pane {
23 | overflow: hidden;
24 |
25 | &.is-expanded {
26 | > v-pane-header {
27 | border-bottom-color: $v-pane-expanded-border-color;
28 |
29 | &::after {
30 | transform: rotate(90deg);
31 | opacity: 0;
32 | }
33 | &::before {
34 | transform: rotate(0deg);
35 | }
36 | }
37 |
38 | > v-pane-content > div {
39 | opacity: 1;
40 | transform: translate3d(0, 0, 0);
41 | }
42 | }
43 |
44 | &[disabled] v-pane-header {
45 | &::after,
46 | &::before {
47 | display: none;
48 | }
49 | }
50 | }
51 |
52 | v-pane-header {
53 | padding: 5px 0;
54 | margin-bottom: $v-accordion-spacing;
55 | border-bottom: 2px solid $v-pane-border-color;
56 | transition: all $v-pane-hover-animation-duration;
57 |
58 | &::after,
59 | &::before {
60 | content: '';
61 | display: block;
62 | position: absolute;
63 | top: 50%;
64 | right: 0;
65 | width: 10px;
66 | height: 1px;
67 | background-color: $v-pane-icon-color;
68 | transform-origin: 50% 50%;
69 | will-change: transform;
70 | transition: all $v-pane-hover-animation-duration;
71 | }
72 |
73 | &::before {
74 | transform: rotate(-90deg);
75 | }
76 |
77 | &:hover,
78 | &:focus {
79 | color: $v-pane-hover-color;
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/vAccordion/styles/vAccordion.scss:
--------------------------------------------------------------------------------
1 | /***************************************
2 | vAccordion
3 | ***************************************/
4 |
5 |
6 | /**
7 | * Example HTML:
8 | *
9 |
10 |
11 |
14 |
15 | [content]
16 |
17 |
18 |
19 | */
20 |
21 |
22 | @import 'settings';
23 | @import 'base';
24 | @import 'theme';
25 |
26 |
--------------------------------------------------------------------------------
/src/vAccordion/vAccordion.js:
--------------------------------------------------------------------------------
1 |
2 | // Config
3 | angular.module('vAccordion.config', [])
4 | .constant('accordionConfig', {
5 | states: {
6 | expanded: 'is-expanded'
7 | },
8 | expandAnimationDuration: 0.5
9 | })
10 | .animation('.is-expanded', [ '$animateCss', 'accordionConfig', function ($animateCss, accordionConfig) {
11 | return {
12 | addClass: function (element, className, done) {
13 | var paneContent = angular.element(element[0].querySelector('v-pane-content')),
14 | paneInner = angular.element(paneContent[0].querySelector('div'));
15 |
16 | var height = paneInner[0].offsetHeight;
17 |
18 | var expandAnimation = $animateCss(paneContent, {
19 | easing: 'ease',
20 | from: { maxHeight: '0px' },
21 | to: { maxHeight: height + 'px' },
22 | duration: accordionConfig.expandAnimationDuration
23 | });
24 |
25 | expandAnimation.start().done(function () {
26 | paneContent.css('max-height', 'none');
27 | done();
28 | });
29 |
30 | return function (isCancelled) {
31 | if (isCancelled) {
32 | paneContent.css('max-height', 'none');
33 | }
34 | };
35 | },
36 | removeClass: function (element, className, done) {
37 | var paneContent = angular.element(element[0].querySelector('v-pane-content')),
38 | paneInner = angular.element(paneContent[0].querySelector('div'));
39 |
40 | var height = paneInner[0].offsetHeight;
41 |
42 | var collapseAnimation = $animateCss(paneContent, {
43 | easing: 'ease',
44 | from: { maxHeight: height + 'px' },
45 | to: { maxHeight: '0px' },
46 | duration: accordionConfig.expandAnimationDuration
47 | });
48 |
49 | collapseAnimation.start().done(done);
50 |
51 | return function (isCancelled) {
52 | if (isCancelled) {
53 | paneContent.css('max-height', '0px');
54 | }
55 | };
56 | }
57 | };
58 | } ]);
59 |
60 |
61 | // Modules
62 | angular.module('vAccordion.directives', []);
63 | angular.module('vAccordion',
64 | [
65 | 'vAccordion.config',
66 | 'vAccordion.directives'
67 | ]);
68 |
--------------------------------------------------------------------------------
/src/vAccordion/vAccordion.prefix:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
--------------------------------------------------------------------------------
/src/vAccordion/vAccordion.suffix:
--------------------------------------------------------------------------------
1 | })(angular);
--------------------------------------------------------------------------------
/test/unit/vAccordion/controllers/vAccordionController.spec.js:
--------------------------------------------------------------------------------
1 | describe('vAccordionController', function () {
2 |
3 | var scope;
4 | var accordion;
5 | var isolateScope;
6 | var controller;
7 |
8 | var generateTemplate = function (options) {
9 | var dafaults = {
10 | attributes: '',
11 | content: ''
12 | };
13 |
14 | if (options) {
15 | angular.extend(dafaults, options);
16 | }
17 |
18 | var template = '\n';
19 | template += dafaults.content + '\n';
20 | template += '';
21 |
22 | return template;
23 | };
24 |
25 | var generatePanes = function (length) {
26 | var samplePanes = [];
27 |
28 | for (var i = 0; i < length; i++) {
29 | var samplePane = {
30 | header: 'Pane header #' + i,
31 | content: 'Pane content #' + i
32 | };
33 |
34 | samplePanes.push(samplePane);
35 | }
36 |
37 | return samplePanes;
38 | };
39 |
40 |
41 | beforeEach(module('vAccordion'));
42 |
43 | beforeEach(inject(function (_$rootScope_, _$compile_) {
44 | scope = _$rootScope_.$new();
45 |
46 | var options = { attributes: 'multiple control="control" onexpand="onExpand(index)" oncollapse="onCollapse(index)"' };
47 | var template = generateTemplate(options);
48 |
49 | accordion = _$compile_(template)(scope);
50 | _$rootScope_.$digest();
51 |
52 | isolateScope = accordion.isolateScope();
53 | controller = accordion.controller('vAccordion');
54 | }));
55 |
56 | afterEach(function () {
57 | scope.$destroy();
58 | });
59 |
60 |
61 | it('should add new pane object to `panes` array', function () {
62 | var samplePane = generatePanes(1)[0];
63 |
64 | expect(isolateScope.panes.length).toBe(0);
65 | controller.addPane(samplePane);
66 | expect(isolateScope.panes.length).toBeGreaterThan(0);
67 | });
68 |
69 |
70 | it('should expand pane and call `onExpand` callback', function () {
71 | var samplePanes = generatePanes(5);
72 | var paneToExpandIndex = 0;
73 | var paneToExpand = samplePanes[paneToExpandIndex];
74 |
75 | for (var i = 0; i < samplePanes.length; i++) {
76 | controller.addPane(samplePanes[i]);
77 | }
78 |
79 | scope.onExpand = jasmine.createSpy('onExpand');
80 | scope.$digest();
81 |
82 | expect(isolateScope.panes[paneToExpandIndex].isExpanded).toBeFalsy();
83 | controller.expand(paneToExpand);
84 | expect(isolateScope.panes[paneToExpandIndex].isExpanded).toBeTruthy();
85 |
86 | expect(scope.onExpand).toHaveBeenCalled();
87 | });
88 |
89 |
90 | it('should collapse pane and call `onCollapse` callback', function () {
91 | var samplePanes = generatePanes(5);
92 | var paneToExpandIndex = 0;
93 | var paneToExpand = samplePanes[paneToExpandIndex];
94 | paneToExpand.isExpanded = true;
95 |
96 | for (var i = 0; i < samplePanes.length; i++) {
97 | controller.addPane(samplePanes[i]);
98 | }
99 |
100 | scope.onCollapse = jasmine.createSpy('onCollapse');
101 | scope.$digest();
102 |
103 | expect(isolateScope.panes[paneToExpandIndex].isExpanded).toBeTruthy();
104 | controller.collapse(paneToExpand);
105 | expect(isolateScope.panes[paneToExpandIndex].isExpanded).toBeFalsy();
106 |
107 | expect(scope.onCollapse).toHaveBeenCalled();
108 | });
109 |
110 | });
111 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/controllers/vPaneController.spec.js:
--------------------------------------------------------------------------------
1 | describe('vPaneController', function () {
2 |
3 | var scope;
4 | var pane;
5 | var isolateScope;
6 | var controller;
7 |
8 | var generatePanes = function (length) {
9 | var samplePanes = [];
10 |
11 | for (var i = 0; i < length; i++) {
12 | var samplePane = {
13 | header: 'Pane header #' + i,
14 | content: 'Pane content #' + i
15 | };
16 |
17 | samplePanes.push(samplePane);
18 | }
19 |
20 | return samplePanes;
21 | };
22 |
23 | var generateTemplate = function (options) {
24 | var dafaults = {
25 | content: '',
26 | accordionAttributes: '',
27 | paneAttributes: ''
28 | };
29 |
30 | if (options) {
31 | angular.extend(dafaults, options);
32 | }
33 |
34 | var template = '\n';
35 | template += '\n';
36 | template += '\n';
37 | template += '' + dafaults.content + '\n';
38 | template += '\n';
39 | template += '';
40 |
41 | return template;
42 | };
43 |
44 |
45 | beforeEach(module('vAccordion'));
46 |
47 | beforeEach(inject(function (_$rootScope_, _$compile_) {
48 | scope = _$rootScope_.$new();
49 |
50 | var options = { accordionAttributes: 'multiple' };
51 | var template = generateTemplate(options);
52 |
53 | var accordion = _$compile_(template)(scope);
54 | pane = accordion.find('v-pane');
55 | _$rootScope_.$digest();
56 |
57 | isolateScope = pane.isolateScope();
58 | controller = pane.controller('vPane');
59 | }));
60 |
61 | afterEach(function () {
62 | scope.$destroy();
63 | });
64 |
65 |
66 | it('should change isExpanded scope value using `expand()`, `collapse()` and `toggle()` methods', function () {
67 | expect(controller.isExpanded()).toBe(false);
68 | controller.expand();
69 | expect(controller.isExpanded()).toBe(true);
70 | controller.collapse();
71 | expect(controller.isExpanded()).toBe(false);
72 | controller.toggle();
73 | expect(controller.isExpanded()).toBe(true);
74 | });
75 |
76 |
77 | it('should change isFocused scope value using `focusPane()` and `blurPane()` methods', function () {
78 | expect(isolateScope.isFocused).toBeFalsy();
79 | controller.focusPane();
80 | expect(isolateScope.isFocused).toBe(true);
81 | controller.blurPane();
82 | expect(isolateScope.isFocused).toBe(false);
83 | });
84 |
85 | });
86 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/directives/vAccordion.spec.js:
--------------------------------------------------------------------------------
1 | describe('vAccordion', function () {
2 |
3 | var $compile;
4 | var $rootScope;
5 | var accordionConfig;
6 | var scope;
7 |
8 | var generateTemplate = function (options) {
9 | var dafaults = {
10 | attributes: '',
11 | content: ''
12 | };
13 |
14 | if (options) {
15 | angular.extend(dafaults, options);
16 | }
17 |
18 | var template = '\n';
19 | template += dafaults.content + '\n';
20 | template += '';
21 |
22 | return template;
23 | };
24 |
25 |
26 |
27 | beforeEach(module('vAccordion'));
28 |
29 | beforeEach(inject(function (_$rootScope_, _$compile_, _accordionConfig_) {
30 | $rootScope = _$rootScope_;
31 | scope = $rootScope.$new();
32 | $compile = _$compile_;
33 | accordionConfig = _accordionConfig_;
34 | }));
35 |
36 | afterEach(function () {
37 | scope.$destroy();
38 | });
39 |
40 |
41 |
42 | it('should transclude scope', function () {
43 | var message = 'Hello World!';
44 |
45 | var template = generateTemplate({ content: '{{ message }}' });
46 | var accordion = $compile(template)(scope);
47 |
48 | scope.message = message;
49 | scope.$digest();
50 |
51 | expect(accordion.html()).toContain(message);
52 | });
53 |
54 |
55 | it('should allow multiple selections to be expanded if `multiple` attribute is defined', function () {
56 | var options = { attributes: 'multiple' };
57 | var template = generateTemplate(options);
58 | var accordion = $compile(template)(scope);
59 |
60 | expect(accordion.isolateScope().allowMultiple).toBe(true);
61 | });
62 |
63 |
64 | it('should add the ARIA `tablist` role', function () {
65 | var template = generateTemplate();
66 | var accordion = $compile(template)(scope);
67 |
68 | expect(accordion.attr('role')).toBe('tablist');
69 | });
70 |
71 |
72 | it('should set `aria-multiselectable` attribute to `true` if `multiple` attribute is defined', function () {
73 | var options = { attributes: 'multiple' };
74 | var template = generateTemplate(options);
75 | var accordion = $compile(template)(scope);
76 |
77 | expect(accordion.attr('aria-multiselectable')).toBeDefined();
78 | });
79 |
80 |
81 | it('should extend custom control object', function () {
82 | scope.control = { someProperty: 'test' };
83 |
84 | var options = { attributes: 'control="control"' };
85 | var template = generateTemplate(options);
86 | var accordion = $compile(template)(scope);
87 |
88 | expect(accordion.isolateScope().internalControl.someProperty).toBeDefined();
89 | expect(accordion.isolateScope().internalControl.someProperty).toBe('test');
90 | });
91 |
92 |
93 | it('should throw an error if the API method is overriden', function () {
94 | scope.control = { toggle: function () {} };
95 |
96 | var options = { attributes: 'control="control"' };
97 | var template = generateTemplate(options);
98 |
99 | expect(function () { $compile(template)(scope); }).toThrow();
100 | });
101 |
102 |
103 | it('should set accordion `internalControl` as `$accordion` property on transcluded scope', function () {
104 | var options = {
105 | attributes: 'id="accordion"',
106 | content: ''
107 | };
108 |
109 | var template = generateTemplate(options);
110 | var accordion = $compile(template)(scope);
111 | var pane = accordion.find('v-pane');
112 | var transcludedScope = pane.scope();
113 |
114 | expect(scope.$accordion).not.toBeDefined();
115 | expect(transcludedScope.$accordion).toBeDefined();
116 | expect(transcludedScope.$accordion.id).toEqual('accordion');
117 | expect(transcludedScope.$accordion.toggle).toBeDefined();
118 | expect(transcludedScope.$accordion.expand).toBeDefined();
119 | expect(transcludedScope.$accordion.collapse).toBeDefined();
120 | expect(transcludedScope.$accordion.expandAll).toBeDefined();
121 | expect(transcludedScope.$accordion.collapseAll).toBeDefined();
122 | expect(transcludedScope.$accordion.hasExpandedPane).toBeDefined();
123 | });
124 |
125 | });
126 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/directives/vPane.spec.js:
--------------------------------------------------------------------------------
1 | describe('vPane', function () {
2 |
3 | var $compile;
4 | var $rootScope;
5 | var accordionConfig;
6 | var scope;
7 |
8 | var generatePanes = function (length) {
9 | var samplePanes = [];
10 |
11 | for (var i = 0; i < length; i++) {
12 | var samplePane = {
13 | header: 'Pane header #' + i,
14 | content: 'Pane content #' + i
15 | };
16 |
17 | samplePanes.push(samplePane);
18 | }
19 |
20 | return samplePanes;
21 | };
22 |
23 | var generateTemplate = function (options) {
24 | var dafaults = {
25 | content: '',
26 | accordionAttributes: '',
27 | paneAttributes: ''
28 | };
29 |
30 | if (options) {
31 | angular.extend(dafaults, options);
32 | }
33 |
34 | var template = '\n';
35 | template += '\n';
36 | template += '\n';
37 | template += '' + dafaults.content + '\n';
38 | template += '\n';
39 | template += '';
40 |
41 | return template;
42 | };
43 |
44 |
45 |
46 | beforeEach(module('vAccordion'));
47 |
48 | beforeEach(inject(function (_$rootScope_, _$compile_, _accordionConfig_) {
49 | $rootScope = _$rootScope_;
50 | scope = $rootScope.$new();
51 | $compile = _$compile_;
52 | accordionConfig = _accordionConfig_;
53 | }));
54 |
55 | afterEach(function () {
56 | scope.$destroy();
57 | });
58 |
59 |
60 |
61 | it('should throw an error if `v-accordion` directive controller can\'t be found', function () {
62 | var template = '';
63 |
64 | expect(function () { $compile(template)(scope); }).toThrow();
65 | });
66 |
67 |
68 | it('should throw an error if `v-pane-header` can\'t be found found', function () {
69 | var template = '\n' +
70 | ' \n' +
71 | '';
72 |
73 | expect(function () { $compile(template)(scope); }).toThrow();
74 | });
75 |
76 |
77 | it('should throw an error if `v-pane-content` can\'t be found found', function () {
78 | var template = '\n' +
79 | ' \n' +
80 | ' \n' +
81 | ' \n' +
82 | '';
83 |
84 | expect(function () { $compile(template)(scope); }).toThrow();
85 | });
86 |
87 |
88 | it('should transclude scope', function () {
89 | var message = 'Hello World!';
90 | var options = { content: '{{ message }}' };
91 |
92 | var template = generateTemplate(options);
93 |
94 | var accordion = $compile(template)(scope);
95 | var paneContent = accordion.find('v-pane-content');
96 |
97 | scope.message = message;
98 | scope.$digest();
99 |
100 | expect(paneContent.html()).toContain(message);
101 | });
102 |
103 |
104 | it('should set pane `internalControl` as `$pane` property on transcluded scope', function () {
105 | var options = { paneAttributes: 'id="pane"' };
106 |
107 | var template = generateTemplate(options);
108 |
109 | var accordion = $compile(template)(scope);
110 | var paneContent = accordion.find('v-pane-content');
111 | var transcludedScope = paneContent.scope();
112 |
113 | expect(scope.$pane).not.toBeDefined();
114 | expect(transcludedScope.$pane).toBeDefined();
115 | expect(transcludedScope.$pane.id).toEqual('pane');
116 | expect(transcludedScope.$pane.toggle).toBeDefined();
117 | expect(transcludedScope.$pane.expand).toBeDefined();
118 | expect(transcludedScope.$pane.collapse).toBeDefined();
119 | expect(transcludedScope.$pane.isExpanded).toBeDefined();
120 | });
121 |
122 |
123 | it('should throw an error if multiple panes has `expanded` attribute, but `multiple` is not set', function () {
124 | var template = '\n' +
125 | ' \n' +
126 | ' \n' +
127 | ' \n' +
128 | ' \n' +
129 | ' \n' +
130 | ' \n' +
131 | ' \n' +
132 | ' \n' +
133 | '';
134 |
135 | expect(function () {
136 | $compile(template)(scope);
137 | scope.$digest();
138 | }).toThrow();
139 | });
140 |
141 |
142 | it('should set `isExpanded` property to `true` if expanded attribute is added and has no value', function () {
143 | var options = { paneAttributes: 'expanded' };
144 | var template = generateTemplate(options);
145 |
146 | var accordion = $compile(template)(scope);
147 | var pane = accordion.find('v-pane');
148 |
149 | expect(pane.isolateScope().isExpanded).toBe(true);
150 | });
151 |
152 |
153 | it('should watch the `isExpanded` value and add `is-expanded` class when it is changed to `true`', function () {
154 | var template = generateTemplate();
155 |
156 | var accordion = $compile(template)(scope);
157 | var pane = accordion.find('v-pane');
158 |
159 | var paneIsolateScope = pane.isolateScope();
160 | paneIsolateScope.$digest();
161 |
162 | expect(pane.hasClass('is-expanded')).toBe(false);
163 |
164 | paneIsolateScope.isExpanded = true;
165 | paneIsolateScope.$digest();
166 |
167 | expect(pane.hasClass('is-expanded')).toBe(true);
168 | });
169 |
170 |
171 | it('should set `isDisabled` property to `true` if `disabled` attribute is set', function () {
172 | var options = { paneAttributes: 'disabled' };
173 | var template = generateTemplate(options);
174 |
175 | var accordion = $compile(template)(scope);
176 | var pane = accordion.find('v-pane');
177 | var paneHeader = accordion.find('v-pane-header');
178 |
179 | var paneIsolateScope = pane.isolateScope();
180 |
181 | paneHeader.click();
182 |
183 | expect(paneIsolateScope.isDisabled).toBe(true);
184 | expect(paneIsolateScope.isExpanded).toBe(false);
185 | });
186 |
187 |
188 | it('should works with `ng-repeat` directive', function () {
189 | var length = 3;
190 |
191 | var template = '\n' +
192 | ' \n' +
193 | ' {{ pane.header }}\n' +
194 | ' {{ pane.content }}\n' +
195 | ' \n' +
196 | '';
197 |
198 | var accordion = $compile(template)(scope);
199 |
200 | scope.panes = generatePanes(length);
201 | scope.$digest();
202 |
203 | expect(accordion.find('v-pane').length).toEqual(length);
204 | });
205 |
206 |
207 | it('should emit `onExpand` and `onCollapse` events', function () {
208 | var options = { accordionAttributes: 'id="accordion"' };
209 | var template = generateTemplate(options);
210 |
211 | var accordion = $compile(template)(scope);
212 | var pane = accordion.find('v-pane');
213 |
214 | var paneIsolateScope = pane.isolateScope();
215 | paneIsolateScope.$digest();
216 |
217 | spyOn(paneIsolateScope, '$emit');
218 |
219 | paneIsolateScope.isExpanded = true;
220 | paneIsolateScope.$digest();
221 |
222 | expect(paneIsolateScope.$emit).toHaveBeenCalledWith('accordion:onExpand');
223 |
224 | paneIsolateScope.isExpanded = false;
225 | paneIsolateScope.$digest();
226 |
227 | expect(paneIsolateScope.$emit).toHaveBeenCalledWith('accordion:onCollapse');
228 | });
229 |
230 | });
231 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/directives/vPaneContent.spec.js:
--------------------------------------------------------------------------------
1 | describe('vPaneContent', function () {
2 |
3 | var $compile;
4 | var $rootScope;
5 | var accordionConfig;
6 | var scope;
7 |
8 | var generateTemplate = function (options) {
9 | var dafaults = {
10 | content: ''
11 | };
12 |
13 | if (options) {
14 | angular.extend(dafaults, options);
15 | }
16 |
17 | var template = '\n';
18 | template += '\n';
19 | template += '\n';
20 | template += '' + dafaults.content + '\n';
21 | template += '\n';
22 | template += '';
23 |
24 | return template;
25 | };
26 |
27 |
28 | beforeEach(module('vAccordion'));
29 |
30 | beforeEach(inject(function (_$rootScope_, _$compile_, _accordionConfig_) {
31 | $rootScope = _$rootScope_;
32 | scope = $rootScope.$new();
33 | $compile = _$compile_;
34 | accordionConfig = _accordionConfig_;
35 | }));
36 |
37 | afterEach(function () {
38 | scope.$destroy();
39 | });
40 |
41 |
42 |
43 | it('should throw an error if `v-pane` directive controller can\'t be found', function () {
44 | var template = '';
45 |
46 | expect(function () { $compile(template)(scope); }).toThrow();
47 | });
48 |
49 |
50 | it('should transclude scope and add inner `div` wrapper', function () {
51 | var message = 'Hello World!';
52 |
53 | var template = generateTemplate({ content: '{{ message }}' });
54 |
55 | var accordion = $compile(template)(scope);
56 | var paneContent = accordion.find('v-pane-content');
57 |
58 | scope.message = message;
59 | scope.$digest();
60 |
61 | expect(paneContent.html()).toContain(message);
62 | expect(paneContent.html()).toContain('');
63 | });
64 |
65 |
66 | it('should add ARIA attributes', function () {
67 | var template = generateTemplate();
68 |
69 | var accordion = $compile(template)(scope);
70 | var paneContent = accordion.find('v-pane-content');
71 |
72 | expect(paneContent.attr('role')).toBe('tabpanel');
73 | expect(paneContent.attr('aria-hidden')).toBeDefined();
74 | });
75 |
76 | it('should expand when `v-pane-header` is clicked', function () {
77 | var template = generateTemplate();
78 |
79 | var accordion = $compile(template)(scope);
80 | var paneHeader = accordion.find('v-pane-header');
81 | var paneContent = accordion.find('v-pane-content');
82 |
83 | expect(paneContent.css('max-height')).toBe('');
84 | expect(paneContent.attr('aria-hidden')).toBe('true');
85 |
86 | paneHeader.click();
87 |
88 | expect(paneContent.css('max-height')).toBe('none');
89 | expect(paneContent.attr('aria-hidden')).toBe('false');
90 | });
91 |
92 | });
93 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/directives/vPaneHeader.spec.js:
--------------------------------------------------------------------------------
1 | describe('vPaneHeader', function () {
2 |
3 | var $compile;
4 | var $rootScope;
5 | var accordionConfig;
6 | var scope;
7 |
8 | var generateTemplate = function (options) {
9 | var dafaults = {
10 | content: ''
11 | };
12 |
13 | if (options) {
14 | angular.extend(dafaults, options);
15 | }
16 |
17 | var template = '\n';
18 | template += '\n';
19 | template += '' + dafaults.content + '\n';
20 | template += '\n';
21 | template += '\n';
22 | template += '';
23 |
24 | return template;
25 | };
26 |
27 |
28 |
29 | beforeEach(module('vAccordion'));
30 |
31 | beforeEach(inject(function (_$rootScope_, _$compile_, _accordionConfig_) {
32 | $rootScope = _$rootScope_;
33 | scope = $rootScope.$new();
34 | $compile = _$compile_;
35 | accordionConfig = _accordionConfig_;
36 | }));
37 |
38 |
39 | afterEach(function () {
40 | scope.$destroy();
41 | });
42 |
43 |
44 |
45 | it('should throw an error if `v-pane` directive controller can\'t be found', function () {
46 | var template = '';
47 |
48 | expect(function () { $compile(template)(scope); }).toThrow();
49 | });
50 |
51 |
52 | it('should transclude scope and create inner `div` wrapper', function () {
53 | var message = 'Hello World!';
54 |
55 | var template = generateTemplate({ content: '{{ message }}' });
56 |
57 | var accordion = $compile(template)(scope);
58 | var paneHeader = accordion.find('v-pane-header');
59 |
60 | scope.message = message;
61 | scope.$digest();
62 |
63 | expect(paneHeader.html()).toContain(message);
64 | expect(paneHeader.html()).toContain('');
65 | });
66 |
67 |
68 | it('should have `role` and `tabindex` attribute', function () {
69 | var template = generateTemplate();
70 |
71 | var accordion = $compile(template)(scope);
72 | var paneHeader = accordion.find('v-pane-header');
73 |
74 | expect(paneHeader.attr('role')).toBe('tab');
75 | expect(paneHeader.attr('tabindex')).toBe('0');
76 | });
77 |
78 |
79 | it('should toggle the pane on click', function () {
80 | var template = generateTemplate();
81 |
82 | var accordion = $compile(template)(scope);
83 | var pane = accordion.find('v-pane');
84 | var paneHeader = accordion.find('v-pane-header');
85 |
86 | var paneIsolateScope = pane.isolateScope();
87 | paneIsolateScope.$digest();
88 |
89 | expect(paneIsolateScope.isExpanded).toBe(false);
90 | expect(paneHeader.attr('aria-selected')).toBe('false');
91 | expect(paneHeader.attr('aria-expanded')).toBe('false');
92 |
93 | paneHeader.click();
94 |
95 | expect(paneIsolateScope.isExpanded).toBe(true);
96 | expect(paneHeader.attr('aria-selected')).toBe('true');
97 | expect(paneHeader.attr('aria-expanded')).toBe('true');
98 | });
99 |
100 | });
101 |
--------------------------------------------------------------------------------
/test/unit/vAccordion/vAccordion.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('vAccordion', function () {
4 |
5 | var dependencies = [];
6 |
7 | var hasModule = function(module) {
8 | return dependencies.indexOf(module) >= 0;
9 | };
10 |
11 |
12 |
13 | beforeEach(function () {
14 | dependencies = angular.module('vAccordion').requires;
15 | });
16 |
17 |
18 |
19 | it('should load config module', function () {
20 | expect(hasModule('vAccordion.config')).toBe(true);
21 | });
22 |
23 |
24 | it('should load directives module', function () {
25 | expect(hasModule('vAccordion.directives')).toBe(true);
26 | });
27 |
28 | });
--------------------------------------------------------------------------------