1 | !function(factory) {
|
---|
2 |
|
---|
3 | if(typeof define === 'function' && define.amd) define(['index', 'jquery', 'jquery.ba-resize'], factory);
|
---|
4 | else TabsAccordion = factory(Index, jQuery);
|
---|
5 | }
|
---|
6 | (function(Index, $) {
|
---|
7 |
|
---|
8 | var count = 0,
|
---|
9 | namespace = 'tabsaccordion',
|
---|
10 | $window = $(window),
|
---|
11 | $html = $(document.documentElement).addClass('js'),
|
---|
12 | $body = $(document.body);
|
---|
13 |
|
---|
14 | $.resize.throttleWindow = false;
|
---|
15 | $.fn.TabsAccordion = function(options) {
|
---|
16 |
|
---|
17 | function T(element, options) {
|
---|
18 |
|
---|
19 | var idNamespace = namespace + '-' + count++,
|
---|
20 | $element = $(element),
|
---|
21 | $panels,
|
---|
22 | $tabs,
|
---|
23 | $tablist,
|
---|
24 | $content,
|
---|
25 | self = {
|
---|
26 | version: '1.2.0',
|
---|
27 | type: ($element.hasClass('accordion') && 'accordion') || ($element.hasClass('tabs') && 'tabs'),
|
---|
28 |
|
---|
29 | create: function() {
|
---|
30 |
|
---|
31 | $panels = $element.children();
|
---|
32 | $tabs = $panels.children(':first-child');
|
---|
33 |
|
---|
34 | if(self.index) var prev = self.index.curr;
|
---|
35 | (self.index = Index($panels.length - 1)).loop = true; // <- index looping for keyboard accessibility
|
---|
36 | if(prev) self.index.set(prev);
|
---|
37 |
|
---|
38 | if(self.type === 'tabs') $element.prepend(($tabs = self.tabsCreateTablist($tabs).children()).end());
|
---|
39 |
|
---|
40 | $tablist = (self.type === 'tabs' ? $tabs.parent() : $element).attr('role', 'tablist');
|
---|
41 |
|
---|
42 | $tabs.attr({
|
---|
43 | 'id': function(index) {
|
---|
44 |
|
---|
45 | return this.id || idNamespace + '-tab-' + index;
|
---|
46 | },
|
---|
47 | 'role': 'tab'
|
---|
48 | });
|
---|
49 |
|
---|
50 | ($content = $panels.map(function(index) {
|
---|
51 |
|
---|
52 | return $(this)
|
---|
53 | .attr({
|
---|
54 | 'aria-labelledby': $tabs[index].id,
|
---|
55 | 'id': this.id || idNamespace + '-panel-' + index,
|
---|
56 | 'role': 'tabpanel'
|
---|
57 | })
|
---|
58 | .children()
|
---|
59 | .slice(1)
|
---|
60 | .wrapAll('<div><div></div></div>')
|
---|
61 | .parent()
|
---|
62 | .parent()
|
---|
63 | .get();
|
---|
64 | }))
|
---|
65 | .each(self.collapse);
|
---|
66 |
|
---|
67 | $element
|
---|
68 | .attr({
|
---|
69 | 'id': element.id || idNamespace,
|
---|
70 | 'tabindex': 0
|
---|
71 | })
|
---|
72 | .on('click.' + idNamespace, self.type === 'accordion' && '> * > :first-child' || '> :first-child > *', function(event) {
|
---|
73 |
|
---|
74 | self.goTo($tabs.index($(event.target).closest($tabs)));
|
---|
75 | })
|
---|
76 | .on('keydown.' + idNamespace, function(event) {
|
---|
77 |
|
---|
78 | // event.target should be the element and not a descendant
|
---|
79 | if(event.target !== element) return;
|
---|
80 |
|
---|
81 | var match = {
|
---|
82 | 37: 'prev',
|
---|
83 | 38: 'prev',
|
---|
84 | 39: 'next',
|
---|
85 | 40: 'next'
|
---|
86 | }[event.keyCode];
|
---|
87 |
|
---|
88 | if(match) {
|
---|
89 |
|
---|
90 | event.preventDefault();
|
---|
91 |
|
---|
92 | self.goTo(self.index[match]);
|
---|
93 | }
|
---|
94 | })
|
---|
95 | .on('resize.' + idNamespace, self.resize)
|
---|
96 | .trigger('create');
|
---|
97 |
|
---|
98 |
|
---|
99 | if(options.saveState) self.extensions.saveState(options.saveState);
|
---|
100 | if(options.responsiveSwitch) self.extensions.responsiveSwitch(options.responsiveSwitch);
|
---|
101 | if(options.hashWatch) self.extensions.hashWatch();
|
---|
102 | if(options.pauseMedia) self.extensions.pauseMedia();
|
---|
103 |
|
---|
104 | if(typeof self.index.curr !== 'number') self.index.set(0);
|
---|
105 |
|
---|
106 | setTimeout(function() {
|
---|
107 |
|
---|
108 | $element.addClass('transition');
|
---|
109 | })
|
---|
110 |
|
---|
111 | return self.expand(self.index.curr);
|
---|
112 | },
|
---|
113 |
|
---|
114 | destroy: function(keepData) {
|
---|
115 |
|
---|
116 | if(self.type === 'tabs') {
|
---|
117 |
|
---|
118 | $element.height('auto');
|
---|
119 |
|
---|
120 | $tablist.remove();
|
---|
121 | }
|
---|
122 | else {
|
---|
123 |
|
---|
124 | $tabs
|
---|
125 | .removeAttr('role')
|
---|
126 | .filter('[id^="' + idNamespace + '"]').removeAttr('id');
|
---|
127 |
|
---|
128 | $tablist.removeAttr('role');
|
---|
129 | }
|
---|
130 |
|
---|
131 | $panels
|
---|
132 | .removeAttr('aria-expanded aria-labelledby role')
|
---|
133 | .filter('[id^="' + idNamespace + '"]').removeAttr('id');
|
---|
134 |
|
---|
135 | $content
|
---|
136 | .children()
|
---|
137 | .children()
|
---|
138 | .unwrap()
|
---|
139 | .unwrap();
|
---|
140 |
|
---|
141 | if(!keepData)
|
---|
142 | $element
|
---|
143 | .removeData(namespace)
|
---|
144 | .removeData('responsiveBreakpoint.' + idNamespace);
|
---|
145 |
|
---|
146 | $element
|
---|
147 | .add([window, document.body])
|
---|
148 | .off('.' + idNamespace)
|
---|
149 | .end()
|
---|
150 | .removeAttr('aria-activedescendant tabindex')
|
---|
151 | .removeClass(self.type)
|
---|
152 | .filter('[id^="' + idNamespace + '"]').removeAttr('id')
|
---|
153 | .end()
|
---|
154 | .trigger('destroy');
|
---|
155 |
|
---|
156 | return self;
|
---|
157 | },
|
---|
158 |
|
---|
159 | resize: function() {
|
---|
160 |
|
---|
161 | if(self.type === 'tabs') $element.height($tablist.outerHeight() + $panels.eq(self.index.curr).outerHeight());
|
---|
162 | else if(self.type === 'accordion' && $panels[self.index.curr].ariaExpanded)
|
---|
163 | $content
|
---|
164 | .eq(self.index.curr)
|
---|
165 | .height($content.eq(self.index.curr).children().outerHeight());
|
---|
166 |
|
---|
167 | return self;
|
---|
168 | },
|
---|
169 |
|
---|
170 | expand: function(index) {
|
---|
171 |
|
---|
172 | var $panel = $panels.eq(index).attr('aria-expanded', $panels[index].ariaExpanded = true);
|
---|
173 |
|
---|
174 | if(self.resize().type === 'tabs') $tabs.eq(index).addClass('current');
|
---|
175 |
|
---|
176 | $element
|
---|
177 | .attr('aria-activedescendant', $panels[self.index.curr].id)
|
---|
178 | .trigger('expand', [index, $panel]);
|
---|
179 |
|
---|
180 | return self;
|
---|
181 | },
|
---|
182 |
|
---|
183 | collapse: function(index) {
|
---|
184 |
|
---|
185 | var $panel = $panels.eq(index).attr('aria-expanded', $panels[index].ariaExpanded = false);
|
---|
186 |
|
---|
187 | if(self.type === 'tabs') $tabs.eq(index).removeClass('current');
|
---|
188 | else $content.eq(index).height(0);
|
---|
189 |
|
---|
190 | $element.trigger('collapse', [index, $panel]);
|
---|
191 |
|
---|
192 | return self;
|
---|
193 | },
|
---|
194 |
|
---|
195 | goTo: function(index) {
|
---|
196 |
|
---|
197 | if(self.index.curr !== index && typeof self.index.curr === 'number') self.collapse(self.index.curr);
|
---|
198 | self.index.set(index);
|
---|
199 |
|
---|
200 | return self[self.type === 'accordion' && $panels.eq(index).prop('ariaExpanded') ? 'collapse' : 'expand'](self.index.curr);
|
---|
201 | },
|
---|
202 |
|
---|
203 | tabsCreateTablist: options.tabsCreateTablist || function(titles) {
|
---|
204 |
|
---|
205 | for(var i = 0, li = ''; i < titles.length; i++) li += '<li>' + titles[i].innerHTML + '</li>';
|
---|
206 |
|
---|
207 | return $('<ul>' + li + '</ul>');
|
---|
208 | },
|
---|
209 |
|
---|
210 | extensions: {
|
---|
211 | hashWatch: function() {
|
---|
212 |
|
---|
213 | var that = {
|
---|
214 | changeHash: function(hash, $target) {
|
---|
215 |
|
---|
216 | var id = $target[0].id;
|
---|
217 |
|
---|
218 | $target[0].id = '';
|
---|
219 | location.hash = hash;
|
---|
220 | $target[0].id = id;
|
---|
221 |
|
---|
222 | return that;
|
---|
223 | },
|
---|
224 |
|
---|
225 | expand: function(hash, event) {
|
---|
226 |
|
---|
227 | var $target = $element.find(hash);
|
---|
228 |
|
---|
229 | if($target.length) {
|
---|
230 |
|
---|
231 | var $panel = $target.closest($panels);
|
---|
232 |
|
---|
233 | if($panel.length) {
|
---|
234 |
|
---|
235 | if(event) event.preventDefault();
|
---|
236 |
|
---|
237 | self.goTo($panels.index($panel));
|
---|
238 | that.changeHash(hash, $target);
|
---|
239 |
|
---|
240 | if(event) {
|
---|
241 | setTimeout(function() {
|
---|
242 |
|
---|
243 | $html
|
---|
244 | .add($body)
|
---|
245 | .animate({
|
---|
246 | scrollTop: $target.offset().top
|
---|
247 | });
|
---|
248 | },250);
|
---|
249 | }
|
---|
250 | }
|
---|
251 | }
|
---|
252 |
|
---|
253 | return that;
|
---|
254 | }
|
---|
255 | };
|
---|
256 |
|
---|
257 | $body
|
---|
258 | // hash anchor activation
|
---|
259 | .on('click.' + idNamespace, 'a[href^="#"]:not([href="#"])', function(event) {
|
---|
260 |
|
---|
261 | that.expand($(event.target).attr('href'), event);
|
---|
262 | })
|
---|
263 | // navigation e.g. back button
|
---|
264 | .on('hashchange.' + idNamespace, function() {
|
---|
265 |
|
---|
266 | that.expand(location.hash);
|
---|
267 | });
|
---|
268 |
|
---|
269 | return that.expand(location.hash);
|
---|
270 | },
|
---|
271 |
|
---|
272 | saveStateLoaded: false,
|
---|
273 | saveState: function(storage) {
|
---|
274 |
|
---|
275 | if(typeof storage !== 'object') return;
|
---|
276 |
|
---|
277 | var state = {
|
---|
278 | remove: function() {
|
---|
279 |
|
---|
280 | storage.removeItem(idNamespace);
|
---|
281 | },
|
---|
282 |
|
---|
283 | load: function() {
|
---|
284 |
|
---|
285 | var item = storage.getItem(idNamespace),
|
---|
286 | data = JSON.parse(item);
|
---|
287 |
|
---|
288 | if(data && data.current) self.index.set(data.current);
|
---|
289 |
|
---|
290 | self.extensions.saveStateLoaded = true;
|
---|
291 | },
|
---|
292 |
|
---|
293 | save: function() {
|
---|
294 |
|
---|
295 | storage.setItem(idNamespace, JSON.stringify({current: self.index.curr, expanded: $panels[self.index.curr].ariaExpanded}));
|
---|
296 | }
|
---|
297 | };
|
---|
298 |
|
---|
299 | // load only once per instance per page load
|
---|
300 | if(!self.extensions.saveStateLoaded) state.load();
|
---|
301 |
|
---|
302 | $window.on('unload.' + idNamespace, state.save);
|
---|
303 |
|
---|
304 | return state;
|
---|
305 | },
|
---|
306 |
|
---|
307 | responsiveSwitch: function(breakpoint) {
|
---|
308 |
|
---|
309 | if(breakpoint === 'tablist') {
|
---|
310 |
|
---|
311 | if(self.type === 'tabs') $element.data('responsiveBreakpoint.' + idNamespace, breakpoint = getTablistWidth());
|
---|
312 | else breakpoint = $element.data('responsiveBreakpoint.' + idNamespace);
|
---|
313 | }
|
---|
314 |
|
---|
315 | function getTablistWidth() {
|
---|
316 |
|
---|
317 | // measure combined width of all tabs instead of single width of tablist, because tabs are floated and can jump to the next line
|
---|
318 | for(var i = 0, width = 0; i < $tabs.length; i++) width += $tabs.eq(i).outerWidth(true);
|
---|
319 |
|
---|
320 | return width;
|
---|
321 | }
|
---|
322 |
|
---|
323 | function switchTo(type) {
|
---|
324 |
|
---|
325 | var current = self.index.curr,
|
---|
326 | expanded = $panels[current].ariaExpanded;
|
---|
327 |
|
---|
328 | self.destroy(true);
|
---|
329 |
|
---|
330 | $element.addClass(self.type = type);
|
---|
331 |
|
---|
332 | self.index.set(current);
|
---|
333 | self.create();
|
---|
334 |
|
---|
335 | $element.trigger('typechange', type);
|
---|
336 | }
|
---|
337 |
|
---|
338 | function checkBreakpoint() {
|
---|
339 |
|
---|
340 | var type = $element.outerWidth() <= breakpoint ? 'accordion' : 'tabs';
|
---|
341 |
|
---|
342 | if(self.type !== type) switchTo(type);
|
---|
343 | }
|
---|
344 |
|
---|
345 | $element.on('resize.' + idNamespace, checkBreakpoint);
|
---|
346 | },
|
---|
347 |
|
---|
348 | pauseMedia: function() {
|
---|
349 |
|
---|
350 | if(typeof Modernizr === 'undefined' || !Modernizr.audio || !Modernizr.video || !$element.find('audio, video').length) return;
|
---|
351 |
|
---|
352 | $element.on('collapse.' + idNamespace, function(event, index, $panel) {
|
---|
353 |
|
---|
354 | $panel.find('audio, video').each(function() {
|
---|
355 |
|
---|
356 | this.pause();
|
---|
357 | });
|
---|
358 | });
|
---|
359 | }
|
---|
360 | }
|
---|
361 | };
|
---|
362 |
|
---|
363 | return self.create();
|
---|
364 | }
|
---|
365 |
|
---|
366 | var options = options || {},
|
---|
367 | args = Array.prototype.slice.call(arguments, 1);
|
---|
368 |
|
---|
369 | return this.each(function(index) {
|
---|
370 |
|
---|
371 | var $this = $(this);
|
---|
372 |
|
---|
373 | // method call : instantiation
|
---|
374 | return $this.data(namespace) ? $this.data(namespace)[options].apply(this, args) : $this.data(namespace, T(this, options));
|
---|
375 | });
|
---|
376 | }
|
---|
377 | }); |
---|