Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

722 строки
19 KiB

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