Fix r74454: convert $wgCacheEpoch to a UNIX timestamp before comparing it with other...
[mediawiki.git] / resources / jquery.ui / jquery.ui.tabs.js
blob1f94d52a250e6fd00e78e5fbe7e2c9f90f6c55df
1 /*
2  * jQuery UI Tabs 1.8.2
3  *
4  * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
5  * Dual licensed under the MIT (MIT-LICENSE.txt)
6  * and GPL (GPL-LICENSE.txt) licenses.
7  *
8  * http://docs.jquery.com/UI/Tabs
9  *
10  * Depends:
11  *      jquery.ui.core.js
12  *      jquery.ui.widget.js
13  */
14 (function($) {
16 var tabId = 0,
17         listId = 0;
19 function getNextTabId() {
20         return ++tabId;
23 function getNextListId() {
24         return ++listId;
27 $.widget("ui.tabs", {
28         options: {
29                 add: null,
30                 ajaxOptions: null,
31                 cache: false,
32                 cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
33                 collapsible: false,
34                 disable: null,
35                 disabled: [],
36                 enable: null,
37                 event: 'click',
38                 fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
39                 idPrefix: 'ui-tabs-',
40                 load: null,
41                 panelTemplate: '<div></div>',
42                 remove: null,
43                 select: null,
44                 show: null,
45                 spinner: '<em>Loading&#8230;</em>',
46                 tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a></li>'
47         },
48         _create: function() {
49                 this._tabify(true);
50         },
52         _setOption: function(key, value) {
53                 if (key == 'selected') {
54                         if (this.options.collapsible && value == this.options.selected) {
55                                 return;
56                         }
57                         this.select(value);
58                 }
59                 else {
60                         this.options[key] = value;
61                         this._tabify();
62                 }
63         },
65         _tabId: function(a) {
66                 return a.title && a.title.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '') ||
67                         this.options.idPrefix + getNextTabId();
68         },
70         _sanitizeSelector: function(hash) {
71                 return hash.replace(/:/g, '\\:'); // we need this because an id may contain a ":"
72         },
74         _cookie: function() {
75                 var cookie = this.cookie || (this.cookie = this.options.cookie.name || 'ui-tabs-' + getNextListId());
76                 return $.cookie.apply(null, [cookie].concat($.makeArray(arguments)));
77         },
79         _ui: function(tab, panel) {
80                 return {
81                         tab: tab,
82                         panel: panel,
83                         index: this.anchors.index(tab)
84                 };
85         },
87         _cleanup: function() {
88                 // restore all former loading tabs labels
89                 this.lis.filter('.ui-state-processing').removeClass('ui-state-processing')
90                                 .find('span:data(label.tabs)')
91                                 .each(function() {
92                                         var el = $(this);
93                                         el.html(el.data('label.tabs')).removeData('label.tabs');
94                                 });
95         },
97         _tabify: function(init) {
99                 this.list = this.element.find('ol,ul').eq(0);
100                 this.lis = $('li:has(a[href])', this.list);
101                 this.anchors = this.lis.map(function() { return $('a', this)[0]; });
102                 this.panels = $([]);
104                 var self = this, o = this.options;
106                 var fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash
107                 this.anchors.each(function(i, a) {
108                         var href = $(a).attr('href');
110                         // For dynamically created HTML that contains a hash as href IE < 8 expands
111                         // such href to the full page url with hash and then misinterprets tab as ajax.
112                         // Same consideration applies for an added tab with a fragment identifier
113                         // since a[href=#fragment-identifier] does unexpectedly not match.
114                         // Thus normalize href attribute...
115                         var hrefBase = href.split('#')[0], baseEl;
116                         if (hrefBase && (hrefBase === location.toString().split('#')[0] ||
117                                         (baseEl = $('base')[0]) && hrefBase === baseEl.href)) {
118                                 href = a.hash;
119                                 a.href = href;
120                         }
122                         // inline tab
123                         if (fragmentId.test(href)) {
124                                 self.panels = self.panels.add(self._sanitizeSelector(href));
125                         }
127                         // remote tab
128                         else if (href != '#') { // prevent loading the page itself if href is just "#"
129                                 $.data(a, 'href.tabs', href); // required for restore on destroy
131                                 // TODO until #3808 is fixed strip fragment identifier from url
132                                 // (IE fails to load from such url)
133                                 $.data(a, 'load.tabs', href.replace(/#.*$/, '')); // mutable data
135                                 var id = self._tabId(a);
136                                 a.href = '#' + id;
137                                 var $panel = $('#' + id);
138                                 if (!$panel.length) {
139                                         $panel = $(o.panelTemplate).attr('id', id).addClass('ui-tabs-panel ui-widget-content ui-corner-bottom')
140                                                 .insertAfter(self.panels[i - 1] || self.list);
141                                         $panel.data('destroy.tabs', true);
142                                 }
143                                 self.panels = self.panels.add($panel);
144                         }
146                         // invalid tab href
147                         else {
148                                 o.disabled.push(i);
149                         }
150                 });
152                 // initialization from scratch
153                 if (init) {
155                         // attach necessary classes for styling
156                         this.element.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all');
157                         this.list.addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
158                         this.lis.addClass('ui-state-default ui-corner-top');
159                         this.panels.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom');
161                         // Selected tab
162                         // use "selected" option or try to retrieve:
163                         // 1. from fragment identifier in url
164                         // 2. from cookie
165                         // 3. from selected class attribute on <li>
166                         if (o.selected === undefined) {
167                                 if (location.hash) {
168                                         this.anchors.each(function(i, a) {
169                                                 if (a.hash == location.hash) {
170                                                         o.selected = i;
171                                                         return false; // break
172                                                 }
173                                         });
174                                 }
175                                 if (typeof o.selected != 'number' && o.cookie) {
176                                         o.selected = parseInt(self._cookie(), 10);
177                                 }
178                                 if (typeof o.selected != 'number' && this.lis.filter('.ui-tabs-selected').length) {
179                                         o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected'));
180                                 }
181                                 o.selected = o.selected || (this.lis.length ? 0 : -1);
182                         }
183                         else if (o.selected === null) { // usage of null is deprecated, TODO remove in next release
184                                 o.selected = -1;
185                         }
187                         // sanity check - default to first tab...
188                         o.selected = ((o.selected >= 0 && this.anchors[o.selected]) || o.selected < 0) ? o.selected : 0;
190                         // Take disabling tabs via class attribute from HTML
191                         // into account and update option properly.
192                         // A selected tab cannot become disabled.
193                         o.disabled = $.unique(o.disabled.concat(
194                                 $.map(this.lis.filter('.ui-state-disabled'),
195                                         function(n, i) { return self.lis.index(n); } )
196                         )).sort();
198                         if ($.inArray(o.selected, o.disabled) != -1) {
199                                 o.disabled.splice($.inArray(o.selected, o.disabled), 1);
200                         }
202                         // highlight selected tab
203                         this.panels.addClass('ui-tabs-hide');
204                         this.lis.removeClass('ui-tabs-selected ui-state-active');
205                         if (o.selected >= 0 && this.anchors.length) { // check for length avoids error when initializing empty list
206                                 this.panels.eq(o.selected).removeClass('ui-tabs-hide');
207                                 this.lis.eq(o.selected).addClass('ui-tabs-selected ui-state-active');
209                                 // seems to be expected behavior that the show callback is fired
210                                 self.element.queue("tabs", function() {
211                                         self._trigger('show', null, self._ui(self.anchors[o.selected], self.panels[o.selected]));
212                                 });
213                                 
214                                 this.load(o.selected);
215                         }
217                         // clean up to avoid memory leaks in certain versions of IE 6
218                         $(window).bind('unload', function() {
219                                 self.lis.add(self.anchors).unbind('.tabs');
220                                 self.lis = self.anchors = self.panels = null;
221                         });
223                 }
224                 // update selected after add/remove
225                 else {
226                         o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected'));
227                 }
229                 // update collapsible
230                 this.element[o.collapsible ? 'addClass' : 'removeClass']('ui-tabs-collapsible');
232                 // set or update cookie after init and add/remove respectively
233                 if (o.cookie) {
234                         this._cookie(o.selected, o.cookie);
235                 }
237                 // disable tabs
238                 for (var i = 0, li; (li = this.lis[i]); i++) {
239                         $(li)[$.inArray(i, o.disabled) != -1 &&
240                                 !$(li).hasClass('ui-tabs-selected') ? 'addClass' : 'removeClass']('ui-state-disabled');
241                 }
243                 // reset cache if switching from cached to not cached
244                 if (o.cache === false) {
245                         this.anchors.removeData('cache.tabs');
246                 }
248                 // remove all handlers before, tabify may run on existing tabs after add or option change
249                 this.lis.add(this.anchors).unbind('.tabs');
251                 if (o.event != 'mouseover') {
252                         var addState = function(state, el) {
253                                 if (el.is(':not(.ui-state-disabled)')) {
254                                         el.addClass('ui-state-' + state);
255                                 }
256                         };
257                         var removeState = function(state, el) {
258                                 el.removeClass('ui-state-' + state);
259                         };
260                         this.lis.bind('mouseover.tabs', function() {
261                                 addState('hover', $(this));
262                         });
263                         this.lis.bind('mouseout.tabs', function() {
264                                 removeState('hover', $(this));
265                         });
266                         this.anchors.bind('focus.tabs', function() {
267                                 addState('focus', $(this).closest('li'));
268                         });
269                         this.anchors.bind('blur.tabs', function() {
270                                 removeState('focus', $(this).closest('li'));
271                         });
272                 }
274                 // set up animations
275                 var hideFx, showFx;
276                 if (o.fx) {
277                         if ($.isArray(o.fx)) {
278                                 hideFx = o.fx[0];
279                                 showFx = o.fx[1];
280                         }
281                         else {
282                                 hideFx = showFx = o.fx;
283                         }
284                 }
286                 // Reset certain styles left over from animation
287                 // and prevent IE's ClearType bug...
288                 function resetStyle($el, fx) {
289                         $el.css({ display: '' });
290                         if (!$.support.opacity && fx.opacity) {
291                                 $el[0].style.removeAttribute('filter');
292                         }
293                 }
295                 // Show a tab...
296                 var showTab = showFx ?
297                         function(clicked, $show) {
298                                 $(clicked).closest('li').addClass('ui-tabs-selected ui-state-active');
299                                 $show.hide().removeClass('ui-tabs-hide') // avoid flicker that way
300                                         .animate(showFx, showFx.duration || 'normal', function() {
301                                                 resetStyle($show, showFx);
302                                                 self._trigger('show', null, self._ui(clicked, $show[0]));
303                                         });
304                         } :
305                         function(clicked, $show) {
306                                 $(clicked).closest('li').addClass('ui-tabs-selected ui-state-active');
307                                 $show.removeClass('ui-tabs-hide');
308                                 self._trigger('show', null, self._ui(clicked, $show[0]));
309                         };
311                 // Hide a tab, $show is optional...
312                 var hideTab = hideFx ?
313                         function(clicked, $hide) {
314                                 $hide.animate(hideFx, hideFx.duration || 'normal', function() {
315                                         self.lis.removeClass('ui-tabs-selected ui-state-active');
316                                         $hide.addClass('ui-tabs-hide');
317                                         resetStyle($hide, hideFx);
318                                         self.element.dequeue("tabs");
319                                 });
320                         } :
321                         function(clicked, $hide, $show) {
322                                 self.lis.removeClass('ui-tabs-selected ui-state-active');
323                                 $hide.addClass('ui-tabs-hide');
324                                 self.element.dequeue("tabs");
325                         };
327                 // attach tab event handler, unbind to avoid duplicates from former tabifying...
328                 this.anchors.bind(o.event + '.tabs', function() {
329                         var el = this, $li = $(this).closest('li'), $hide = self.panels.filter(':not(.ui-tabs-hide)'),
330                                         $show = $(self._sanitizeSelector(this.hash));
332                         // If tab is already selected and not collapsible or tab disabled or
333                         // or is already loading or click callback returns false stop here.
334                         // Check if click handler returns false last so that it is not executed
335                         // for a disabled or loading tab!
336                         if (($li.hasClass('ui-tabs-selected') && !o.collapsible) ||
337                                 $li.hasClass('ui-state-disabled') ||
338                                 $li.hasClass('ui-state-processing') ||
339                                 self._trigger('select', null, self._ui(this, $show[0])) === false) {
340                                 this.blur();
341                                 return false;
342                         }
344                         o.selected = self.anchors.index(this);
346                         self.abort();
348                         // if tab may be closed
349                         if (o.collapsible) {
350                                 if ($li.hasClass('ui-tabs-selected')) {
351                                         o.selected = -1;
353                                         if (o.cookie) {
354                                                 self._cookie(o.selected, o.cookie);
355                                         }
357                                         self.element.queue("tabs", function() {
358                                                 hideTab(el, $hide);
359                                         }).dequeue("tabs");
360                                         
361                                         this.blur();
362                                         return false;
363                                 }
364                                 else if (!$hide.length) {
365                                         if (o.cookie) {
366                                                 self._cookie(o.selected, o.cookie);
367                                         }
368                                         
369                                         self.element.queue("tabs", function() {
370                                                 showTab(el, $show);
371                                         });
373                                         self.load(self.anchors.index(this)); // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171
374                                         
375                                         this.blur();
376                                         return false;
377                                 }
378                         }
380                         if (o.cookie) {
381                                 self._cookie(o.selected, o.cookie);
382                         }
384                         // show new tab
385                         if ($show.length) {
386                                 if ($hide.length) {
387                                         self.element.queue("tabs", function() {
388                                                 hideTab(el, $hide);
389                                         });
390                                 }
391                                 self.element.queue("tabs", function() {
392                                         showTab(el, $show);
393                                 });
394                                 
395                                 self.load(self.anchors.index(this));
396                         }
397                         else {
398                                 throw 'jQuery UI Tabs: Mismatching fragment identifier.';
399                         }
401                         // Prevent IE from keeping other link focussed when using the back button
402                         // and remove dotted border from clicked link. This is controlled via CSS
403                         // in modern browsers; blur() removes focus from address bar in Firefox
404                         // which can become a usability and annoying problem with tabs('rotate').
405                         if ($.browser.msie) {
406                                 this.blur();
407                         }
409                 });
411                 // disable click in any case
412                 this.anchors.bind('click.tabs', function(){return false;});
414         },
416         destroy: function() {
417                 var o = this.options;
419                 this.abort();
420                 
421                 this.element.unbind('.tabs')
422                         .removeClass('ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible')
423                         .removeData('tabs');
425                 this.list.removeClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
427                 this.anchors.each(function() {
428                         var href = $.data(this, 'href.tabs');
429                         if (href) {
430                                 this.href = href;
431                         }
432                         var $this = $(this).unbind('.tabs');
433                         $.each(['href', 'load', 'cache'], function(i, prefix) {
434                                 $this.removeData(prefix + '.tabs');
435                         });
436                 });
438                 this.lis.unbind('.tabs').add(this.panels).each(function() {
439                         if ($.data(this, 'destroy.tabs')) {
440                                 $(this).remove();
441                         }
442                         else {
443                                 $(this).removeClass([
444                                         'ui-state-default',
445                                         'ui-corner-top',
446                                         'ui-tabs-selected',
447                                         'ui-state-active',
448                                         'ui-state-hover',
449                                         'ui-state-focus',
450                                         'ui-state-disabled',
451                                         'ui-tabs-panel',
452                                         'ui-widget-content',
453                                         'ui-corner-bottom',
454                                         'ui-tabs-hide'
455                                 ].join(' '));
456                         }
457                 });
459                 if (o.cookie) {
460                         this._cookie(null, o.cookie);
461                 }
463                 return this;
464         },
466         add: function(url, label, index) {
467                 if (index === undefined) {
468                         index = this.anchors.length; // append by default
469                 }
471                 var self = this, o = this.options,
472                         $li = $(o.tabTemplate.replace(/#\{href\}/g, url).replace(/#\{label\}/g, label)),
473                         id = !url.indexOf('#') ? url.replace('#', '') : this._tabId($('a', $li)[0]);
475                 $li.addClass('ui-state-default ui-corner-top').data('destroy.tabs', true);
477                 // try to find an existing element before creating a new one
478                 var $panel = $('#' + id);
479                 if (!$panel.length) {
480                         $panel = $(o.panelTemplate).attr('id', id).data('destroy.tabs', true);
481                 }
482                 $panel.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide');
484                 if (index >= this.lis.length) {
485                         $li.appendTo(this.list);
486                         $panel.appendTo(this.list[0].parentNode);
487                 }
488                 else {
489                         $li.insertBefore(this.lis[index]);
490                         $panel.insertBefore(this.panels[index]);
491                 }
493                 o.disabled = $.map(o.disabled,
494                         function(n, i) { return n >= index ? ++n : n; });
496                 this._tabify();
498                 if (this.anchors.length == 1) { // after tabify
499                         o.selected = 0;
500                         $li.addClass('ui-tabs-selected ui-state-active');
501                         $panel.removeClass('ui-tabs-hide');
502                         this.element.queue("tabs", function() {
503                                 self._trigger('show', null, self._ui(self.anchors[0], self.panels[0]));
504                         });
505                                 
506                         this.load(0);
507                 }
509                 // callback
510                 this._trigger('add', null, this._ui(this.anchors[index], this.panels[index]));
511                 return this;
512         },
514         remove: function(index) {
515                 var o = this.options, $li = this.lis.eq(index).remove(),
516                         $panel = this.panels.eq(index).remove();
518                 // If selected tab was removed focus tab to the right or
519                 // in case the last tab was removed the tab to the left.
520                 if ($li.hasClass('ui-tabs-selected') && this.anchors.length > 1) {
521                         this.select(index + (index + 1 < this.anchors.length ? 1 : -1));
522                 }
524                 o.disabled = $.map($.grep(o.disabled, function(n, i) { return n != index; }),
525                         function(n, i) { return n >= index ? --n : n; });
527                 this._tabify();
529                 // callback
530                 this._trigger('remove', null, this._ui($li.find('a')[0], $panel[0]));
531                 return this;
532         },
534         enable: function(index) {
535                 var o = this.options;
536                 if ($.inArray(index, o.disabled) == -1) {
537                         return;
538                 }
540                 this.lis.eq(index).removeClass('ui-state-disabled');
541                 o.disabled = $.grep(o.disabled, function(n, i) { return n != index; });
543                 // callback
544                 this._trigger('enable', null, this._ui(this.anchors[index], this.panels[index]));
545                 return this;
546         },
548         disable: function(index) {
549                 var self = this, o = this.options;
550                 if (index != o.selected) { // cannot disable already selected tab
551                         this.lis.eq(index).addClass('ui-state-disabled');
553                         o.disabled.push(index);
554                         o.disabled.sort();
556                         // callback
557                         this._trigger('disable', null, this._ui(this.anchors[index], this.panels[index]));
558                 }
560                 return this;
561         },
563         select: function(index) {
564                 if (typeof index == 'string') {
565                         index = this.anchors.index(this.anchors.filter('[href$=' + index + ']'));
566                 }
567                 else if (index === null) { // usage of null is deprecated, TODO remove in next release
568                         index = -1;
569                 }
570                 if (index == -1 && this.options.collapsible) {
571                         index = this.options.selected;
572                 }
574                 this.anchors.eq(index).trigger(this.options.event + '.tabs');
575                 return this;
576         },
578         load: function(index) {
579                 var self = this, o = this.options, a = this.anchors.eq(index)[0], url = $.data(a, 'load.tabs');
581                 this.abort();
583                 // not remote or from cache
584                 if (!url || this.element.queue("tabs").length !== 0 && $.data(a, 'cache.tabs')) {
585                         this.element.dequeue("tabs");
586                         return;
587                 }
589                 // load remote from here on
590                 this.lis.eq(index).addClass('ui-state-processing');
592                 if (o.spinner) {
593                         var span = $('span', a);
594                         span.data('label.tabs', span.html()).html(o.spinner);
595                 }
597                 this.xhr = $.ajax($.extend({}, o.ajaxOptions, {
598                         url: url,
599                         success: function(r, s) {
600                                 $(self._sanitizeSelector(a.hash)).html(r);
602                                 // take care of tab labels
603                                 self._cleanup();
605                                 if (o.cache) {
606                                         $.data(a, 'cache.tabs', true); // if loaded once do not load them again
607                                 }
609                                 // callbacks
610                                 self._trigger('load', null, self._ui(self.anchors[index], self.panels[index]));
611                                 try {
612                                         o.ajaxOptions.success(r, s);
613                                 }
614                                 catch (e) {}
615                         },
616                         error: function(xhr, s, e) {
617                                 // take care of tab labels
618                                 self._cleanup();
620                                 // callbacks
621                                 self._trigger('load', null, self._ui(self.anchors[index], self.panels[index]));
622                                 try {
623                                         // Passing index avoid a race condition when this method is
624                                         // called after the user has selected another tab.
625                                         // Pass the anchor that initiated this request allows
626                                         // loadError to manipulate the tab content panel via $(a.hash)
627                                         o.ajaxOptions.error(xhr, s, index, a);
628                                 }
629                                 catch (e) {}
630                         }
631                 }));
633                 // last, so that load event is fired before show...
634                 self.element.dequeue("tabs");
636                 return this;
637         },
639         abort: function() {
640                 // stop possibly running animations
641                 this.element.queue([]);
642                 this.panels.stop(false, true);
644                 // "tabs" queue must not contain more than two elements,
645                 // which are the callbacks for the latest clicked tab...
646                 this.element.queue("tabs", this.element.queue("tabs").splice(-2, 2));
648                 // terminate pending requests from other tabs
649                 if (this.xhr) {
650                         this.xhr.abort();
651                         delete this.xhr;
652                 }
654                 // take care of tab labels
655                 this._cleanup();
656                 return this;
657         },
659         url: function(index, url) {
660                 this.anchors.eq(index).removeData('cache.tabs').data('load.tabs', url);
661                 return this;
662         },
664         length: function() {
665                 return this.anchors.length;
666         }
670 $.extend($.ui.tabs, {
671         version: '1.8.2'
675  * Tabs Extensions
676  */
679  * Rotate
680  */
681 $.extend($.ui.tabs.prototype, {
682         rotation: null,
683         rotate: function(ms, continuing) {
685                 var self = this, o = this.options;
686                 
687                 var rotate = self._rotate || (self._rotate = function(e) {
688                         clearTimeout(self.rotation);
689                         self.rotation = setTimeout(function() {
690                                 var t = o.selected;
691                                 self.select( ++t < self.anchors.length ? t : 0 );
692                         }, ms);
693                         
694                         if (e) {
695                                 e.stopPropagation();
696                         }
697                 });
698                 
699                 var stop = self._unrotate || (self._unrotate = !continuing ?
700                         function(e) {
701                                 if (e.clientX) { // in case of a true click
702                                         self.rotate(null);
703                                 }
704                         } :
705                         function(e) {
706                                 t = o.selected;
707                                 rotate();
708                         });
710                 // start rotation
711                 if (ms) {
712                         this.element.bind('tabsshow', rotate);
713                         this.anchors.bind(o.event + '.tabs', stop);
714                         rotate();
715                 }
716                 // stop rotation
717                 else {
718                         clearTimeout(self.rotation);
719                         this.element.unbind('tabsshow', rotate);
720                         this.anchors.unbind(o.event + '.tabs', stop);
721                         delete this._rotate;
722                         delete this._unrotate;
723                 }
725                 return this;
726         }
729 })(jQuery);