├── TapDancers.jpg
├── README.md
├── demo.html
├── jquery.tabdancer.css
└── jquery.tabdancer.js
/TapDancers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmkenz/TabDancer/HEAD/TapDancers.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TabDancer
2 | A Responsive Tabs Solution for jQuery
3 |
4 | Too many tabs to fit on one line? Hide the ones that don't fit, and reveal them on command. Shrink your viewport so that the tabs no longer fit on one line. A new 'more' tab will replace the hidden tabs.
5 |
6 | Demo: http://jamesmckenzie.ca/TabDancer
7 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Too many tabs to fit on one line? Hide the ones that don't fit, and reveal them on command.
28 | To see the magic, shrink your viewport so that the tabs no longer fit on one line. A new 'more' tab will replace the hidden tabs.
29 |
39 |
40 | Tab content 1
41 |
42 |
43 | Tab content 2
44 |
45 |
46 | Tab content 3
47 |
48 |
49 | Tab content 4
50 |
51 |
52 | Tab content 5
53 |
54 |
55 | Tab content 6
56 |
57 |
58 |
59 |
60 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/jquery.tabdancer.css:
--------------------------------------------------------------------------------
1 | .js-tabs-container {
2 | position: relative;
3 | }
4 |
5 | .js-tab-content {
6 | display: none;
7 | }
8 |
9 | .no-js .js-tab-content {
10 | display: block;
11 | }
12 |
13 | .tabs {
14 | margin: 0;
15 | padding: 0;
16 | list-style: none;
17 | display: block;
18 | overflow: hidden;
19 | position: relative;
20 | }
21 |
22 | .tabs:before,
23 | .tabs:after {
24 | content: "";
25 | display: table;
26 | }
27 |
28 | .tabs:after {
29 | clear: both;
30 | zoom: 1;
31 | }
32 |
33 | .tabs li {
34 | float: left;
35 | margin-left: 5px;
36 | }
37 |
38 | .tabs li:first-child {
39 | margin-left: 0;
40 | }
41 |
42 | .tabs li,
43 | .tabs li a,
44 | .js-tab-stack li.tab-available {
45 | display: block;
46 | }
47 |
48 | .tabs li a {
49 | padding: 0.55em 1.1em;
50 | text-decoration: none;
51 | border-radius: 4px;
52 | color: #337AB7;
53 | }
54 |
55 | .tabs li a:hover {
56 | background: #f5f5f5;
57 | }
58 |
59 | .tabs .tab-active a,
60 | .tabs .tab-active a:hover {
61 | cursor: default;
62 | color: #fff;
63 | background: #337AB7;
64 | }
65 |
66 | .js-tab-content-container:focus {
67 | outline: none;
68 | }
69 |
70 | li.js-tab-toggler {
71 | display: none;
72 | float: right;
73 | }
74 |
75 | .js-tab-stack li {
76 | display: none;
77 | }
78 |
79 | .js-tab-stack li.tab-available,
80 | .js-tab-stack li.tab-active,
81 | .js-tab-stack-open li,
82 | .js-tab-stack li.js-tab-toggler {
83 | display: block;
84 | }
85 |
86 | .js-tab-toggler a:after {
87 | content: " +";
88 | }
89 |
90 | .js-tab-stack-open li.js-tab-clone,
91 | .js-tab-stack-open-complete li {
92 | clear: left;
93 | }
94 |
95 | .js-tab-clone-container {
96 | position: absolute;
97 | top: 0;
98 | left: 0;
99 | visibility: hidden;
100 | }
101 |
102 | .js-tab-stack-open li.js-tab-clone,
103 | .js-tab-stack-open li.js-ready-for-anim,
104 | .js-tab-stack-open-complete li {
105 | margin-left: 0;
106 | }
107 |
108 | li.js-ready-for-anim {
109 | position: absolute;
110 | transition: all 500ms cubic-bezier(0.130, 0.965, 0.380, 0.985);
111 | }
112 |
113 | li.js-ready-for-anim a:hover {
114 | background: none;
115 | }
116 |
117 | .js-tab-stack-open li.tab-active a:hover {
118 | background: #337AB7;
119 | }
120 |
121 | .js-tab-stack-open li.js-tab-toggler {
122 | display: none;
123 | }
--------------------------------------------------------------------------------
/jquery.tabdancer.js:
--------------------------------------------------------------------------------
1 | /*Tab Dancer jQuery plugin
2 | // Author: James McKenzie
3 | // jamesmckenzie.ca
4 | // Version: 0.8;
5 | */
6 |
7 | $.fn.TabDancer = function(){
8 |
9 | var $tabsContainer = $(this),
10 | $tabs = $tabsContainer.find('li'),
11 | $activeTab = $tabs.find('.tab-active').first(),
12 | $tabToggler = $('
'),
13 | $tabTogglerLink = $tabToggler.find('a').first(),
14 | textMore = $tabsContainer.data('more-tabs-text') || "More";
15 |
16 | //Add accessibility attributes and show active content
17 | $tabsContainer.attr('role', 'tablist');
18 | $tabs.each(function(){
19 | var $tab = $(this),
20 | targetID = $tab.children('a').attr('href').replace('#', '');
21 |
22 | if($tab.is('.tab-active')){
23 | $('#'+targetID).show();
24 | }
25 | $tab.attr('role', 'tab');
26 | $tab.attr('aria-controls', targetID);
27 | });
28 |
29 | //Add tab toggler element
30 | $tabTogglerLink.append('
'+textMore+'');
31 | $tabs.last().after($tabToggler);
32 |
33 | //Responsive tab toggler to reveal hidden tabs
34 | var toggleTabs = function(event){
35 | event.preventDefault();
36 |
37 | //expand the tabs
38 | if(!$tabsContainer.hasClass('js-tab-stack-open')){
39 | //Allow auto height on the tabs container
40 | $tabsContainer.css('height', 'auto');
41 |
42 | $tabsContainer.toggleClass('js-tab-stack').toggleClass('js-tab-stack-open');
43 |
44 | //ANIMATE TRANSITION
45 | var $tabClonesContainer = $('
');
46 | $tabsContainer.append($tabClonesContainer);
47 | //Get target y-axis positions of each item
48 | for(var i = 0; i < $tabs.length; i++){
49 |
50 | $tabs[i].tabClone = $($tabs[i]).clone();
51 | $tabs[i].tabClone.addClass('js-tab-clone');
52 | $tabClonesContainer.append($tabs[i].tabClone);
53 |
54 | }
55 |
56 | //Create vertical space during animation
57 | $tabsContainer.height($tabClonesContainer.height());
58 |
59 | var $tabClones = [];
60 |
61 | //Before we measure the left offset of each item, ensure they all fit on one line temporarily
62 | $tabsContainer.css('width', '9999px');
63 |
64 | for(var i = $tabs.length-1; i > 0; i = i-1){
65 | $tabs[i].targetTop = $tabs[i].tabClone[0].offsetTop;
66 | $tabs[i].sourceLeft = $tabs[i].offsetLeft - parseInt($($tabs[i]).css('margin-left'));
67 |
68 | if(i==1){
69 | //Return the tabs container width to normal
70 | $tabsContainer.css('width','');
71 | }
72 |
73 |
74 | $($tabs[i]).css({'top':'0', 'left':$tabs[i].sourceLeft});
75 | $($tabs[i]).addClass('js-ready-for-anim');
76 |
77 | (function(x){
78 | setTimeout(function(){
79 | $($tabs[x]).css({'top':$tabs[x].targetTop, 'left':'0'});
80 | }, 1); /*This tiny delay allows the CSS animation to register a change*/
81 | })(i);
82 |
83 | //If we are complete all our animations
84 | if(i == 1) {
85 | setTimeout(function(){
86 | $tabsContainer.addClass('js-tab-stack-open-complete');
87 | $('.js-ready-for-anim').each(function(){
88 |
89 | $(this).css({
90 | 'top':'',
91 | 'left':''
92 | })
93 | .removeClass('js-ready-for-anim');
94 | });
95 | $tabClonesContainer.remove();
96 |
97 | //Return the tabs container height to normal
98 | $tabsContainer.css('height','');
99 |
100 | }, 500); /*This delay must match the animation length*/
101 | }
102 | }
103 |
104 | //Move keyboard focus to active tab
105 | $(this).siblings('.tab-active').find('a').focus();
106 | } else {
107 | //Collapse the tabs
108 |
109 | for(var i = $tabs.length-1; i > 0; i = i-1){
110 | $tabs[i].sourceTop = $tabs[i].offsetTop;
111 |
112 | $($tabs[i]).css({'top':$tabs[i].sourceTop, 'left':'0'});
113 | $($tabs[i]).addClass('js-ready-for-anim');
114 | }
115 |
116 | $tabsContainer.removeClass('js-tab-stack-open-complete')
117 | .addClass('js-tab-stack')
118 | .removeClass('js-tab-stack-open');
119 |
120 | checkTabsWidth();
121 |
122 | //Set the active tab's 'left' property to be right after the last 'available' tab
123 | var rightMostLocation = $tabs[$tabs.length - 1].sourceLeft;
124 |
125 | for(var i = 1; i < $tabs.length; i = i+1){
126 | if(!$($tabs[i]).hasClass('tab-available') && $($tabs[i-1]).hasClass('tab-available')){
127 | rightMostLocation = $tabs[i].sourceLeft;
128 | }
129 |
130 | if($($tabs[i]).hasClass('tab-active') && !$($tabs[i-1]).hasClass('tab-available') ){
131 | $tabs[i].sourceLeft = rightMostLocation;
132 | }
133 | }
134 |
135 |
136 | for(var i = $tabs.length-1; i > 0; i = i-1){
137 |
138 | (function(x){
139 | setTimeout(function(){
140 | $($tabs[x]).css('top', '0');
141 | $($tabs[x]).css('left', $tabs[x].sourceLeft);
142 | }, 1);
143 | })(i);
144 |
145 |
146 |
147 | if(i == 1) {
148 | setTimeout(function(){
149 | $('.js-ready-for-anim').each(function(){
150 | $(this).css({
151 | 'top':'',
152 | 'left':''
153 | });
154 | });
155 | $('.js-ready-for-anim').removeClass('js-ready-for-anim');
156 | }, 500);
157 | }
158 | }
159 |
160 | }
161 | };
162 |
163 | //Check width of tabs to see if they all fit in viewport
164 | var checkTabsWidth = function(){
165 |
166 | if(!$tabsContainer.hasClass('js-tab-stack-open')) {
167 |
168 | $tabsContainer.removeClass('js-tab-stack');
169 |
170 | var listWidth = $tabsContainer.width() - 10,
171 | itemWidth = 0,
172 | eachItemWidth = new Array(),
173 | eachItemObject = new Array(),
174 | marginAmount,
175 | activeItemWidth = $tabsContainer.children('.tab-active').width(),
176 | i = 0;
177 |
178 | //Set fixed height to tabs container to hide any items that overflow to a new line
179 | var $tabsContainerClone = $tabsContainer.clone();
180 | $tabsContainerClone.find('li').not(':first-child').remove();
181 | $tabsContainerClone.css('visibility', 'hidden');
182 | $tabsContainer.after($tabsContainerClone);
183 | $tabsContainer.css('height', $tabsContainerClone.height());
184 | $tabsContainerClone.remove();
185 |
186 | $tabsContainer.children('li').not('.js-tab-toggler').each(function() {
187 | $el = $(this);
188 | $el.removeClass('tab-available');
189 | marginAmount = parseInt($el.css('margin-left')) + parseInt($el.css('margin-right'));
190 | eachItemWidth[i] = $el.outerWidth() + marginAmount;
191 | eachItemObject[i] = $el;
192 | itemWidth = itemWidth + eachItemWidth[i];
193 | i ++;
194 | });
195 |
196 | if(itemWidth > listWidth) {
197 | $tabsContainer.addClass('js-tab-stack').find('.tabs-more').show();
198 |
199 | //Check again to be sure they still fit. If not, hide the .tabs-more text
200 | itemWidth = 0;
201 |
202 | $tabsContainer.children('.tab-active, .js-tab-toggler').each(function() {
203 | $el = $(this);
204 | marginAmount = parseInt($el.css('margin-left')) + parseInt($el.css('margin-right'));
205 | itemWidth = itemWidth + $el.outerWidth() + marginAmount;
206 |
207 | if($el.hasClass('tab-active')){
208 | $el.addClass('tab-available');
209 | }
210 | });
211 |
212 | if(itemWidth > listWidth) {
213 | $tabsContainer.find('.tabs-more').hide();
214 | }
215 |
216 | //See if any other tabs can fit in (set to 'tab-available')
217 | var tabTogglerWidth = $tabsContainer.children('.js-tab-toggler').width(),
218 | remainingWidth = listWidth - activeItemWidth - tabTogglerWidth;
219 |
220 | for (var x = 0; x < eachItemObject.length; x++) {
221 |
222 | if(!eachItemObject[x].hasClass('tab-active')) {
223 | remainingWidth = remainingWidth - eachItemWidth[x];
224 |
225 | if (remainingWidth > 0) {
226 | eachItemObject[x].addClass('tab-available');
227 | }
228 | }
229 |
230 | }
231 |
232 | } else { //Tabs all fit!
233 | $tabsContainer.removeClass('js-tab-stack');
234 | }
235 | }
236 | }
237 |
238 | $(window).resize(function() {
239 | if(this.resizeTO) clearTimeout(this.resizeTO);
240 | this.resizeTO = setTimeout(function() {
241 | $(this).trigger('resizeEnd');
242 | }, 500);
243 | });
244 |
245 | $(window).bind('resizeEnd', function() {
246 | /*----Convert Responsive Tabs to drop-down if needed ---*/
247 | checkTabsWidth();
248 | });
249 |
250 | $(window).trigger('resizeEnd');
251 |
252 |
253 |
254 |
255 | //Enable tab changes
256 | $tabsContainer.find('li a').click(function(event) {
257 | event.preventDefault();
258 | var $self = $(this),
259 | $parent = $self.parent('li');
260 |
261 | if(!$parent.hasClass('js-tab-toggler')) {
262 | //Find currently visible tab content and hides it
263 | $parent.siblings('.tab-active').each(function() {
264 | currentTabContent = $(this).children('a').attr('href');
265 | $(currentTabContent).hide();
266 | });
267 | $parent.siblings().attr('aria-selected', 'false');
268 | $parent.siblings().removeClass("tab-active");
269 |
270 | //Show new tab content
271 | $parent.attr('aria-selected', 'true');
272 | $parent.addClass("tab-active");
273 | $(this.hash).fadeIn();
274 | $self.focus();
275 |
276 | //Collapse stacked tabs, if they are stacked
277 | if($tabsContainer.hasClass('js-tab-stack-open')){
278 | toggleTabs(event);
279 | checkTabsWidth();
280 | }
281 |
282 | } else {
283 | toggleTabs(event);
284 | }
285 |
286 | });
287 | };
288 |
--------------------------------------------------------------------------------