puffer/puffer_pages

View on GitHub
app/assets/javascripts/puffer/right-tabs-src.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * Tabs widget for RightJS (http://rightjs.org/ui/tabs)
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
var Tabs = RightJS.Tabs = (function(document, parseInt, RightJS) {
/**
 * This module defines the basic widgets constructor
 * it creates an abstract proxy with the common functionality
 * which then we reuse and override in the actual widgets
 *
 * Copyright (C) 2010-2011 Nikolay Nemshilov
 */

/**
 * The tabs init-script
 *
 * Copyright (C) 2010-2011 Nikolay Nemshilov
 */
var R  = RightJS,
    $  = RightJS.$,
    $$ = RightJS.$$,
    $w = RightJS.$w,
    $E = RightJS.$E,
    Fx       = RightJS.Fx,
    Object   = RightJS.Object,
    Browser  = RightJS.Browser,
    isArray  = RightJS.isArray,
    isNumber = RightJS.isNumber,
    Class    = RightJS.Class,
    Element  = RightJS.Element,
    Cookie   = RightJS.Cookie;







/**
 * The widget units constructor
 *
 * @param String tag-name or Object methods
 * @param Object methods
 * @return Widget wrapper
 */
function Widget(tag_name, methods) {
  if (!methods) {
    methods = tag_name;
    tag_name = 'DIV';
  }

  /**
   * An Abstract Widget Unit
   *
   * Copyright (C) 2010 Nikolay Nemshilov
   */
  var AbstractWidget = new RightJS.Class(RightJS.Element.Wrappers[tag_name] || RightJS.Element, {
    /**
     * The common constructor
     *
     * @param Object options
     * @param String optional tag name
     * @return void
     */
    initialize: function(key, options) {
      this.key = key;
      var args = [{'class': 'rui-' + key}];

      // those two have different constructors
      if (!(this instanceof RightJS.Input || this instanceof RightJS.Form)) {
        args.unshift(tag_name);
      }
      this.$super.apply(this, args);

      if (RightJS.isString(options)) {
        options = RightJS.$(options);
      }

      // if the options is another element then
      // try to dynamically rewrap it with our widget
      if (options instanceof RightJS.Element) {
        this._ = options._;
        if ('$listeners' in options) {
          options.$listeners = options.$listeners;
        }
        options = {};
      }
      this.setOptions(options, this);

      return (RightJS.Wrapper.Cache[RightJS.$uid(this._)] = this);
    },

  // protected

    /**
     * Catches the options
     *
     * @param Object user-options
     * @param Element element with contextual options
     * @return void
     */
    setOptions: function(options, element) {
      element = element || this;
      RightJS.Options.setOptions.call(this,
        RightJS.Object.merge(options, eval("("+(
          element.get('data-'+ this.key) || '{}'
        )+")"))
      );
      return this;
    }
  });

  /**
   * Creating the actual widget class
   *
   */
  var Klass = new RightJS.Class(AbstractWidget, methods);

  // creating the widget related shortcuts
  RightJS.Observer.createShortcuts(Klass.prototype, Klass.EVENTS || []);

  return Klass;
}


/**
 * A shared module to create textual spinners
 *
 * Copyright (C) 2010-2011 Nikolay Nemshilov
 */
var Spinner = new RightJS.Class(RightJS.Element, {
  /**
   * Constructor
   *
   * @param Number optional spinner size (4 by default)
   * @return void
   */
  initialize: function(size) {
    this.$super('div', {'class': 'rui-spinner'});
    this.dots = [];

    for (var i=0; i < (size || 4); i++) {
      this.dots.push(new RightJS.Element('div'));
    }

    this.dots[0].addClass('glowing');
    this.insert(this.dots);
    RightJS(this.shift).bind(this).periodical(300);
  },

  /**
   * Shifts the spinner elements
   *
   * @return void
   */
  shift: function() {
    if (this.visible()) {
      var dot = this.dots.pop();
      this.dots.unshift(dot);
      this.insert(dot, 'top');
    }
  }
});


/**
 * The basic tabs handling engine
 *
 * Copyright (C) 2009-2011 Nikolay Nemshilov
 */
var Tabs = new Widget('UL', {
  extend: {
    version: '2.2.0',

    EVENTS: $w('select hide load disable enable add remove move'),

    Options: {
      idPrefix:       '',      // the tab-body elements id prefix
      tabsElement:    null,    // the tabs list element reference, in case it's situated somewhere else

      resizeFx:       'both',  // 'slide', 'fade', 'both' or null for no fx
      resizeDuration: 400,     // the tab panels resize fx duration

      scrollTabs:     false,   // use the tabs list scrolling
      scrollDuration: 400,     // the tabs scrolling fx duration

      selected:       null,    // the index of the currently opened tab, by default will check url, cookies or set 0
      disabled:       null,    // list of disabled tab indexes

      closable:       false,   // set true if you want a close icon on your tabs

      loop:           false,   // put a delay in ms to make it autostart the slideshow loop
      loopPause:      true,    // make the loop get paused when user hovers the tabs with mouse

      url:            false,   // a common remote tabs url template, should have the %{id} placeholder
      cache:          false,   // marker if the remote tabs should be cached

      Xhr:            null,    // the xhr addtional options
      Cookie:         null     // set the cookie options if you'd like to keep the last selected tab index in cookies
    },

    // scans and automatically intializes the tabs
    rescan: function(scope) {
      $(scope || document).find('.rui-tabs,*[data-tabs]').each(function(element) {
        element = element instanceof Tabs ? element : new Tabs(element);
      });
    }
  },

  /**
   * The basic constructor
   *
   * @param element or id
   * @param Object options
   */
  initialize: function(element, options) {
    this
      .$super('tabs', element)
      .addClass('rui-tabs');

    this.isHarmonica = this._.tagName === 'DL';
    this.isCarousel  = this.hasClass('rui-tabs-carousel');
    this.isSimple    = !this.isHarmonica && !this.isCarousel;

    this
      .findTabs()
      .initScrolls()
      .findCurrent()
      .setStyle('visibility:visible');

    if (this.options.disabled) {
      this.disable(this.options.disabled);
    }

    if (this.options.loop) {
      this.startLoop();
    }
  },

  /**
   * Shows the given tab
   *
   * @param integer tab index or a Tabs.Tab instance
   * @return Tabs this
   */
  select: function(tab) {
    return this.callTab(tab, 'select');
  },

  /**
   * Disables the given tab
   *
   * @param integer tab index or a Tabs.Tab instance or a list of them
   * @return Tabs this
   */
  disable: function(tab) {
    return this.callTab(tab, 'disable');
  },

  /**
   * Enables the given tab
   *
   * @param integer tab index or a Tabs.Tab instance or a list of them
   * @return Tabs this
   */
  enable: function(tab) {
    return this.callTab(tab, 'enable');
  },

  /**
   * Returns the reference to the currently opened tab
   *
   * @return Tab tab or undefined
   */
  current: function() {
    return this.tabs.first('current');
  },

  /**
   * Returns the list of enabled tabs
   *
   * @return Array of enabled tabs
   */
  enabled: function() {
    return this.tabs.filter('enabled');
  },

// protected

  // calls the tab (or tabs) method
  callTab: function(tabs, method) {
    R(isArray(tabs) ? tabs : [tabs]).each(function(tab) {
      if (isNumber(tab)) { tab = this.tabs[tab]; }
      if (tab && tab instanceof Tab) {
        tab[method]();
      }
    }, this);

    return this;
  },

  // finds and interconnects the tabs
  findTabs: function() {
    this.tabsList = this.isHarmonica ? this :
      $(this.options.tabsElement) || this.first('.rui-tabs-list') ||
        (this.first('UL') || $E('UL').insertTo(this)).addClass('rui-tabs-list');

    this.tabs = R([]);

    this.tabsList.children(this.isHarmonica ? 'dt' : null).map(function(node) {
      this.tabs.push(new Tab(node, this));
    }, this);

    // removing the whitespaces so the didn't screw with the margins
    for (var i=0, list = this.tabsList.get('childNodes'); i < list.length; i++) {
      if (list[i].nodeType == 3) { this.tabsList._.removeChild(list[i]); }
    }

    return this;
  }
});


/**
 * A single tab handling object
 *
 * Copyright (C) 2009-2011 Nikolay Nemshilov
 */
var Tab = Tabs.Tab = new Class(Element, {
  extend: {
    autoId: 0
  },

  /**
   * Constructor
   *
   * @param Element the tab's element
   * @param Tabs the main element
   * @return void
   */
  initialize: function(element, main) {
    this.$super(element._);
    this.addClass('rui-tabs-tab');

    this.main  = main;
    this.link  = this.first('a');
    this.id    = this.link.get('href').split('#')[1] || Tab.autoId++;
    this.panel = new Panel(this.findPanel(), this);

    if (this.current()) {
      this.select();
    }

    // adding the 'close' icon onto the tab if needed
    if (main.options.closable) {
      this.link.insert($E('div', {
        'class': 'rui-tabs-tab-close-icon', 'html': '&times;'
      }).onClick(R(this.remove).bind(this)));
    }

    this.onClick(this._clicked);
  },

  select: function() {
    if (this.enabled()) {
      var prev_tab = this.main.current();
      if (prev_tab) {
        prev_tab.removeClass('rui-tabs-current').fire('hide');
      }

      this.addClass('rui-tabs-current');
      this.main.scrollToTab(this);
      this.panel.show();
    }

    return this.fire('select');
  },

  disable: function() {
    return this.addClass('rui-tabs-disabled').fire('disable');
  },

  enable: function() {
    return this.removeClass('rui-tabs-disabled').fire('enable');
  },

  disabled: function() {
    return !this.enabled();
  },

  enabled: function() {
    return !this.hasClass('rui-tabs-disabled');
  },

  current: function() {
    return this.hasClass('rui-tabs-current');
  },

  remove: function(event) {
    if (event) { event.stop(); }

    // switching to the next available sibling
    if (this.current()) {
      var enabled = this.main.enabled();
      var sibling = enabled[enabled.indexOf(this) + 1] || enabled[enabled.indexOf(this)-1];

      if (sibling) {
        sibling.select();
      }
    }

    // removing the tab out of the list
    this.main.tabs.splice(this.main.tabs.indexOf(this), 1);
    this.panel.remove();

    return this.fire('remove').$super();
  },

// protected

  // handles the clicks on the tabs
  _clicked: function(event) {
    event.stop();
    return this.select();
  },

  // searches for a panel for the tab
  findPanel: function() {
    var main = this.main, panel_id = main.options.idPrefix + this.id, panel;

    if (main.isHarmonica) {
      var next = this.next();
      panel = (next && next._.tagName === 'DD') ? next : $E('DD').insertTo(this, 'after');
    } else {
      panel = $(panel_id) || $E(main._.tagName === 'UL' ? 'LI' : 'DIV').insertTo(main);
    }

    return panel.set('id', panel_id);
  },

  // returns the tab width, used for the scrolling calculations
  width: function() {
    var next = this.next();

    if (next) {
      return next.position().x - this.position().x;
    } else {
      return this.size().x + 1;
    }
  }

});


/**
 * The tab panels behavior logic
 *
 * Copyright (C) 2009-2011 Nikolay Nemshilov
 */
var Panel = Tabs.Panel = new Class(Element, {

  /**
   * Basic constructor
   *
   * @param Element panel-element
   * @param Tab the tab object
   * @return void
   */
  initialize: function(element, tab) {
    this.$super(element._);
    this.addClass('rui-tabs-panel');

    this.tab = tab;
    this.id  = this.get('id');
  },

  // shows the panel
  show: function() {
    return this.resizing(function() {
      this.tab.main.find('.rui-tabs-panel').each(function(panel) {
        panel[panel === this ? 'addClass' : 'removeClass']('rui-tabs-current');
      }, this);
    });
  },

  // updates the panel content
  update: function(content) {
    // don't use resize if it's some other hidden tab was loaded asynch
    if (this.tab.current()) {
      this.resizing(function() {
        Element.prototype.update.call(this, content||'');
      });
    } else {
      this.$super(content||'');
    }

    return this;
  },

  // locks the panel with a spinner locker
  lock: function() {
    this.insert(this.locker(), 'top');
  },

// protected

  resizing: function(callback) {
    var controller = this.tab.main;

    if (controller.__working) { return this.resizing.bind(this, callback).delay(100); }

    var options    = controller.options;
    var prev_panel = controller.first('.rui-tabs-panel.rui-tabs-current');
    var this_panel = this;
    var swapping   = prev_panel !== this_panel;
    var loading    = this.first('div.rui-tabs-panel-locker');

    // sometimes it looses the parent on remote tabs
    if (this_panel.parent().hasClass('rui-tabs-resizer')) {
      this_panel.insertTo(prev_panel.parent());
    }

    if (options.resizeFx && RightJS.Fx && prev_panel && (swapping || loading)) {
      controller.__working = true;
      var unlock = function() { controller.__working = false; };

      // calculating the visual effects durations
      var fx_name  = (options.resizeFx === 'both' && loading) ? 'slide' : options.resizeFx;
      var duration = options.resizeDuration; duration = Fx.Durations[duration] || duration;
      var resize_duration = fx_name === 'fade' ? 0 : fx_name === 'slide' ? duration : duration / 2;
      var fade_duration   = duration - resize_duration;

      if (fx_name !== 'slide') {
        this_panel.setStyle({opacity: 0});
      }

      // saving the previous sizes
      var prev_panel_height = (controller.isHarmonica && swapping) ? 0 : prev_panel.size().y;

      // applying the changes
      callback.call(this);

      // getting the new size
      var new_panel_height  = this_panel.size().y;
      var fx_wrapper = null;
      var hide_wrapper = null;
      var prev_back = null;

      if (fx_name !== 'fade' && prev_panel_height !== new_panel_height) {
        // preserving the whole element size so it didn't jump when we are tossing the tabs around
        controller._.style.height = controller.size().y + 'px';

        // wrapping the element with an overflowed element to visualize the resize
        fx_wrapper = $E('div', {
          'class': 'rui-tabs-resizer',
          'style': 'height: '+ prev_panel_height + 'px'
        });

        // in case of harmonica nicely hidding the previous panel
        if (controller.isHarmonica && swapping) {
          prev_panel.addClass('rui-tabs-current');
          hide_wrapper = $E('div', {'class': 'rui-tabs-resizer'});
          hide_wrapper._.style.height = prev_panel.size().y + 'px';
          prev_back = function() {
            hide_wrapper.replace(prev_panel.removeClass('rui-tabs-current'));
          };
          prev_panel.wrap(hide_wrapper);

          fx_wrapper._.style.height = '0px';
        }

        this_panel.wrap(fx_wrapper);

        // getting back the auto-size so we could resize it
        controller._.style.height = 'auto';

      } else {
        // removing the resize duration out of the equasion
        rezise_duration = 0;
        duration = fade_duration;
      }

      var counter = 0;
      var set_back = function() {
        if (fx_wrapper) {
          if (fx_name == 'both' && !counter) {
            return counter ++;
          }

          fx_wrapper.replace(this_panel);
        }

        unlock();
      };

      if (hide_wrapper) {
        hide_wrapper.morph({height: '0px'},
          {duration: resize_duration, onFinish: prev_back});
      }

      if (fx_wrapper) {
        fx_wrapper.morph({height: new_panel_height + 'px'},
          {duration: resize_duration, onFinish: set_back});
      }

      if (fx_name !== 'slide') {
        this_panel.morph.bind(this_panel, {opacity: 1},
          {duration: fade_duration, onFinish: set_back}
            ).delay(resize_duration);
      }

      if (!fx_wrapper && fx_name === 'slide') {
        set_back();
      }

    } else {
      callback.call(this);
    }

    return this;
  },

  // builds the locker element
  locker: function() {
    return this._locker || (this._locker =
      $E('div', {'class': 'rui-tabs-panel-locker'}).insert(new Spinner(5))
    );
  }
});


/**
 * Contains the tabs scrolling functionality
 *
 * NOTE: different types of tabs have different scrolling behavior
 *       simple tabs just scroll the tabs line without actually picking
 *       any tab. But the carousel tabs scrolls to the next/previous
 *       tabs on the list.
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
Tabs.include({
  /**
   * Shows the next tab
   *
   * @return Tabs this
   */
  next: function() {
    return this.pickTab(+1);
  },

  /**
   * Shows the preveious tab
   *
   * @return Tabs this
   */
  prev: function() {
    return this.pickTab(-1);
  },

  /**
   * Scrolls the tabs to the left
   *
   * @return Tabs this
   */
  scrollLeft: function() {
    if (!this.prevButton.hasClass('rui-tabs-scroller-disabled')) {
      this[this.isCarousel ? 'prev' : 'justScroll'](+0.6);
    }
    return this;
  },

  /**
   * Scrolls the tabs to the right
   *
   * @return Tabs this
   */
  scrollRight: function() {
    if (!this.nextButton.hasClass('rui-tabs-scroller-disabled')) {
      this[this.isCarousel ? 'next' : 'justScroll'](-0.6);
    }
    return this;
  },

// protected

  // overloading the init script to add the scrollbar support
  initScrolls: function() {
    if ((this.scrollable = (this.options.scrollTabs || this.isCarousel))) {
      this.buildScroller();
    }

    return this;
  },

  // builds the tabs scroller block
  buildScroller: function() {
    if (!(
      (this.prevButton = this.first('.rui-tabs-scroller-prev')) &&
      (this.nextButton = this.first('.rui-tabs-scroller-next'))
    )) {
      this.prevButton = $E('div', {'class': 'rui-tabs-scroller-prev', 'html': '&laquo;'});
      this.nextButton = $E('div', {'class': 'rui-tabs-scroller-next', 'html': '&raquo;'});

      // using a dummy element to insert the scroller in place of the tabs list
      $E('div').insertTo(this.tabsList, 'before')
        .replace(
          $E('div', {'class': 'rui-tabs-scroller'}).insert([
            this.prevButton, this.nextButton, this.scroller = $E('div', {
              'class': 'rui-tabs-scroller-body'
            }).insert(this.tabsList)
          ])
        ).remove();
    }

    this.prevButton.onClick(R(this.scrollLeft).bind(this));
    this.nextButton.onClick(R(this.scrollRight).bind(this));
  },

  // picks the next/prev non-disabled available tab
  pickTab: function(pos) {
    var current = this.current();
    if (current && current.enabled()) {
      var enabled_tabs = this.enabled();
      var tab = enabled_tabs[enabled_tabs.indexOf(current) + pos];
      if (tab) { tab.select(); }
    }
  },

  // scrolls the tabs line to make the tab visible
  scrollToTab: function(tab) {
    if (this.scroller) {
      // calculating the previous tabs widths
      var tabs_width      = 0;
      for (var i=0; i < this.tabs.length; i++) {
        tabs_width += this.tabs[i].width();
        if (this.tabs[i] === tab) { break; }
      }

      // calculating the scroll (the carousel tabs should be centralized)
      var available_width = this.scroller.size().x;
      var scroll = (this.isCarousel ? (available_width/2 + tab.width()/2) : available_width) - tabs_width;

      // check if the tab doesn't need to be scrolled
      if (!this.isCarousel) {
        var current_scroll  = parseInt(this.tabsList.getStyle('left') || 0, 10);

        if (scroll >= current_scroll && scroll < (current_scroll + available_width - tab.width())) {
          scroll = current_scroll;
        } else if (current_scroll > -tabs_width && current_scroll <= (tab.width() - tabs_width)) {
          scroll = tab.width() - tabs_width;
        }
      }

      this.scrollTo(scroll);
    }
  },

  // just scrolls the scrollable area onto the given number of scrollable area widths
  justScroll: function(size) {
    if (!this.scroller) { return this; }
    var current_scroll  = parseInt(this.tabsList.getStyle('left') || 0, 10);
    var available_width = this.scroller.size().x;

    this.scrollTo(current_scroll + available_width * size);
  },

  // scrolls the tabs list to the position
  scrollTo: function(scroll) {
    // checking the constraints
    var available_width = this.scroller.size().x;
    var overall_width   = this.tabs.map('width').sum();

    if (scroll < (available_width - overall_width)) {
      scroll = available_width - overall_width;
    }
    if (scroll > 0) { scroll = 0; }

    // applying the scroll
    this.tabsList.morph({left: scroll+'px'}, {duration: this.options.scrollDuration});

    this.checkScrollButtons(overall_width, available_width, scroll);
  },

  // checks the scroll buttons
  checkScrollButtons: function(overall_width, available_width, scroll) {
    var has_prev = false, has_next = false;

    if (this.isCarousel) {
      var enabled = this.enabled();
      var current = enabled.first('current');

      if (current) {
        var index = enabled.indexOf(current);

        has_prev = index > 0;
        has_next = index < enabled.length - 1;
      }
    } else {
      has_prev = scroll !== 0;
      has_next = scroll > (available_width - overall_width);
    }

    this.prevButton[has_prev ? 'removeClass' : 'addClass']('rui-tabs-scroller-disabled');
    this.nextButton[has_next ? 'removeClass' : 'addClass']('rui-tabs-scroller-disabled');
  }

});


/**
 * This module handles the current tab state saving/restoring processes
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
function get_cookie_indexes() {
  return R(RightJS.Cookie ? (Cookie.get('right-tabs-indexes') || '').split(',') : []);
}

function save_tab_in_cookies(options, tabs, event) {
  if (RightJS.Cookie) {
    var indexes = get_cookie_indexes();
    indexes = indexes.without.apply(indexes, tabs.map('id'));
    indexes.push(event.target.id);
    Cookie.set('right-tabs-indexes', indexes.uniq().join(','), options);
  }
}

Tabs.include({

// protected

  // searches and activates the current tab
  findCurrent: function() {
    var enabled = this.enabled(), current = (
      this.tabs[this.options.selected] ||
      this.tabs[this.urlIndex()]       ||
      this.tabs[this.cookieIndex()]    ||
      enabled.first('current')         ||
      enabled[0]
    );

    if (current) {
      current.select();
    }

    // initializing the cookies storage if set
    if (this.options.Cookie) {
      this.onSelect(R(save_tab_in_cookies).curry(this.options.Cookie, this.tabs));
    }

    return this;
  },

  // tries to find the current tab index in the url hash
  urlIndex: function() {
    var index = -1, id = document.location.href.split('#')[1];

    if (id) {
      for (var i=0; i < this.tabs.length; i++) {
        if (this.tabs[i].id == id) {
          index = i;
          break;
        }
      }
    }

    return index;
  },

  // tries to find the current tab index in the cookies storage
  cookieIndex: function() {
    var index = -1;

    if (this.options.Cookie) {
      var indexes = get_cookie_indexes();
      for (var i=0; i < this.tabs.length; i++) {
        if (indexes.include(this.tabs[i].id)) {
          index = i;
          break;
        }
      }
    }

    return index;
  }

});


/**
 * This module handles the tabs cration and removing processes
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
Tabs.include({
  /**
   * Creates a new tab
   *
   * USAGE:
   *   With the #add method you have to specify the tab title
   *   optional content (possibly empty or null) and some options
   *   The options might have the following keys
   *
   *     * id - the tab/panel id (will use the idPrefix option for the panels)
   *     * url - a remote tab content address
   *     * position - an integer position of the tab in the stack
   *
   * @param String title
   * @param mixed content
   * @param Object options
   * @return Tabs this
   */
  add: function(title, content, options) {
    options = options || {};

    // creating the new tab element
    var element = $E(this.isHarmonica ? 'dt' : 'li').insert(
      $E('a', {html: title, href: options.url || '#'+(options.id||'')}
    )).insertTo(this.tabsList);

    // creating the actual tab instance
    var tab = new Tab(element, this);
    tab.panel.update(content||'');
    this.tabs.push(tab);
    tab.fire('add');

    // moving the tab in place if asked
    if ('position' in options) {
      this.move(tab, options.position);
    }

    return this;
  },

  /**
   * Moves the given tab to the given position
   *
   * NOTE if the position is not within the tabs range then it will do nothing
   *
   * @param mixed tab index or a tab instance
   * @param Integer position
   * @return Tabs this
   */
  move: function(tab, position) {
    tab = this.tabs[tab] || tab;

    if (this.tabs[position] && this.tabs[position] !== tab) {
      // moving the tab element
      this.tabs[position].insert(tab, (position === this.tabs.length-1) ? 'after' : 'before');

      // inserting the panel after the tab if it's a harmonica
      if (this.isHarmonica) {
        tab.insert(tab.panel, 'after');
      }

      // moving the tab in the registry
      this.tabs.splice(this.tabs.indexOf(tab), 1);
      this.tabs.splice(position, 0, tab);

      tab.fire('move', {index: position});
    }

    return this;
  },

  /**
   * Removes the given tab
   *
   * @param integer tab index or a Tabs.Tab instance or a list of them
   * @return Tabs this
   */
  remove: function(tab) {
    return this.callTab(tab, 'remove');
  }

});


/**
 * This module contains the remote tabs loading logic
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
var old_select = Tab.prototype.select;

Tab.include({

  // wrapping the original mehtod, to catch the remote requests
  select: function() {
    if (this.dogPiling(arguments)) { return this; }

    var result  = old_select.apply(this, arguments);
    var url     = R(this.link.get('href'));
    var options = this.main.options;

    // building the url
    if (url.includes('#')) {
      url = options.url ? options.url.replace('%{id}', url.split('#')[1]) : null;
    }

    // if there is an actual url and no ongoing request or a cache, starting the request
    if (url && !this.request && !(options.cache || this.cache)) {
      this.panel.lock();

      try { // basically that's for the development tests, so the IE browsers didn't get screwed on the test page

        this.request = new RightJS.Xhr(url, Object.merge({method: 'get'}, options.Xhr))
          .onComplete(R(function(response) {
            if (this.main.__working) {
              return arguments.callee.bind(this, response).delay(100);
            }

            this.panel.update(response.text);

            this.request = null; // removing the request marker so it could be rerun
            if (options.cache) {
              this.cache = true;
            }

            this.fire('load');
          }).bind(this)
        ).send();

      } catch(e) { if (!Browser.OLD) { throw(e); } }
    }

    return result;
  },

// protected

  dogPiling: function(args) {
    if (this.main.__working) {
      if (this.main.__timeout) {
        this.main.__timeout.cancel();
      }

      this.main.__timeout = R(function(args) {
        this.select.apply(this, args);
      }).bind(this, args).delay(100);

      return true;
    }

    return (this.main.__timeout = null);
  }

});


/**
 * This module handles the slide-show loop feature for the Tabs
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
Tabs.include({

  /**
   * Starts the slideshow loop
   *
   * @param Number optional delay in ms
   * @return Tabs this
   */
  startLoop: function(delay) {
    if (!delay && !this.options.loop) { return this; }

    // attaching the loop pause feature
    if (this.options.loopPause) {
      this._stopLoop  = this._stopLoop  || R(this.stopLoop).bind(this, true);
      this._startLoop = this._startLoop || R(this.startLoop).bind(this, delay);

      this.forgetHovers().on({
        mouseover: this._stopLoop,
        mouseout:  this._startLoop
      });
    }

    if (this.timer) { this.timer.stop(); }

    this.timer = R(function() {
      var enabled = this.enabled();
      var current = this.current();
      var next    = enabled[enabled.indexOf(current)+1];

      this.select(next || enabled.first());

    }).bind(this).periodical(this.options.loop || delay);

    return this;
  },

  /**
   * Stops the slideshow loop
   *
   * @return Tabs this
   */
  stopLoop: function(event, pause) {
    if (this.timer) {
      this.timer.stop();
      this.timer = null;
    }
    if (!pause && this._startLoop) {
      this.forgetHovers();
    }
  },

// private
  forgetHovers: function() {
    return this
      .stopObserving('mouseover', this._stopLoop)
      .stopObserving('mouseout', this._startLoop);
  }


});


/**
 * The document level hooks for the tabs-egnine
 *
 * Copyright (C) 2009-2010 Nikolay Nemshilov
 */
$(document).onReady(function() {
  Tabs.rescan();
});
(function() {
        var style = document.createElement('style'),
            rules = document.createTextNode("div.rui-spinner,div.rui-spinner div{margin:0;padding:0;border:none;background:none;list-style:none;font-weight:normal;float:none;display:inline-block; *display:inline; *zoom:1;border-radius:.12em;-moz-border-radius:.12em;-webkit-border-radius:.12em}div.rui-spinner{text-align:center;white-space:nowrap;background:#EEE;border:1px solid #DDD;height:1.2em;padding:0 .2em}div.rui-spinner div{width:.4em;height:70%;background:#BBB;margin-left:1px}div.rui-spinner div:first-child{margin-left:0}div.rui-spinner div.glowing{background:#777}.rui-tabs,.rui-tabs-list,.rui-tabs-tab,.rui-tabs-panel,.rui-tabs-scroll-left,.rui-tabs-scroll-right,.rui-tabs-scroll-body,.rui-tabs-panel-locker,.rui-tabs-resizer{margin:0;padding:0;background:none;border:none;list-style:none;display:block;width:auto;height:auto}.rui-tabs{display:block;visibility:hidden;border-bottom:1px solid #CCC}.rui-tabs-resizer{overflow:hidden}.rui-tabs-list{display:block;position:relative;padding:0 .5em;border-bottom:1px solid #CCC;white-space:nowrap}.rui-tabs-list .rui-tabs-tab,.rui-tabs-tab *,.rui-tabs-tab *:hover{display:inline-block; *display:inline; *zoom:1;cursor:pointer;text-decoration:none;vertical-align:center}.rui-tabs-list .rui-tabs-tab{vertical-align:bottom;margin-right:.1em}.rui-tabs-tab a{outline:none;position:relative;border:1px solid #CCC;background:#DDD;color:#444;padding:.3em 1em;border-radius:.3em;-moz-border-radius:.3em;-webkit-border-radius:.3em;border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-bottomright:0;-webkit-border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0}.rui-tabs-tab a:hover{border-color:#CCC;background:#EEE}.rui-tabs-list .rui-tabs-current a,.rui-tabs-list .rui-tabs-current a:hover{font-weight:bold;color:#000;background:#FFF;border-bottom:1px solid #FFF;border-top-width:2px;padding-top:.34em;padding-bottom:.34em;top:1px}.rui-tabs-tab a img{border:none;opacity:.6;filter:alpha(opacity=60)}.rui-tabs-tab a:hover img,.rui-tabs-list .rui-tabs-current a img{opacity:1;filter:alpha(opacity=100)}.rui-tabs-disabled a,.rui-tabs-disabled a:hover{background:#EEE;border-color:#DDD;color:#AAA;cursor:default}.rui-tabs-disabled a img,.rui-tabs-disabled a:hover img{opacity:.5;filter:alpha(opacity=50)}.rui-tabs-tab-close-icon{display:inline-block; *display:inline; *zoom:1;margin-right:-0.5em;margin-left:0.5em;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}.rui-tabs-tab-close-icon:hover{opacity:1;filter:alpha(opacity=100);color:#B00;text-shadow:#888 .15em .15em .2em}.rui-tabs-panel{display:none;position:relative;min-height:4em;padding:.5em 0}.rui-tabs-current{display:block}.rui-tabs-scroller{position:relative;padding:0 1.4em}.rui-tabs-scroller-prev,.rui-tabs-scroller-next{width:1.1em;text-align:center;background:#EEE;color:#666;cursor:pointer;border:1px solid #CCC;border-radius:.2em;-moz-border-radius:.2em;-webkit-border-radius:.2em;position:absolute;bottom:0px;left:0px;padding:0.3em 0;user-select:none;-moz-user-select:none;-webkit-user-select:none}.rui-tabs-scroller-prev:hover,.rui-tabs-scroller-next:hover{color:#000;background:#DDD;border-color:#AAA}.rui-tabs-scroller-prev:active,.rui-tabs-scroller-next:active{background:#eee;border-color:#ccc}.rui-tabs-scroller-next{left:auto;right:0px}.rui-tabs-scroller-disabled,.rui-tabs-scroller-disabled:hover{cursor:default;background:#DDD;border-color:#DDD;color:#AAA}.rui-tabs-scroller-body{overflow:hidden;width:100%;position:relative}.rui-tabs-scroller .rui-tabs-list{padding-left:0;padding-right:0;width:9999em;z-index:10}.rui-tabs-panel-locker{position:absolute;top:0px;left:0px;opacity:0.5;filter:alpha(opacity=50);background:#CCC;width:100%;height:100%;text-align:center}.rui-tabs-panel-locker .rui-spinner{position:absolute;left:44%;top:44%;background:none;border:none;height:2em}.rui-tabs-panel-locker .rui-spinner div{background:#666;width:.65em;margin-left:.15em}.rui-tabs-panel-locker .rui-spinner div.glowing{background:#000}.rui-tabs-carousel .rui-tabs-list{border:none}.rui-tabs-carousel .rui-tabs-tab a,.rui-tabs-carousel .rui-tabs-scroller .rui-tabs-scroller-prev,.rui-tabs-carousel .rui-tabs-scroller .rui-tabs-scroller-next{height:6em;line-height:6em;padding:0;border-bottom:1px solid #ccc;border-radius:.25em;-moz-border-radius:.25em;-webkit-border-radius:.25em}.rui-tabs-carousel .rui-tabs-tab{margin-right:3px}.rui-tabs-carousel .rui-tabs-tab a img{border:1px solid #CCC;vertical-align:middle;margin:.4em;padding:0;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0}.rui-tabs-carousel .rui-tabs-list .rui-tabs-current a{border-width:1px;border-color:#AAA;padding:0;top:auto}.rui-tabs-carousel .rui-tabs-list .rui-tabs-current a img{border-color:#bbb}.rui-tabs-carousel .rui-tabs-panel{text-align:center}dl.rui-tabs{border:none}dt.rui-tabs-tab,dt.rui-tabs-tab a,dt.rui-tabs-tab a:hover{display:block;float:none}dt.rui-tabs-tab a,dt.rui-tabs-tab a:hover{padding:.2em 1em;border:1px solid #ccc;border-radius:.25em;-moz-border-radius:.3em;-webkit-border-radius:.3em}dl.rui-tabs dt.rui-tabs-current a{background:#EEE;border-bottom-left-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-bottomright:0;-webkit-border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0}dl.rui-tabs dd.rui-tabs-current+dt.rui-tabs-tab a{border-top-left-radius:0;border-top-right-radius:0;-moz-border-radius-topleft:0;-moz-border-radius-topright:0;-webkit-border-top-left-radius:0;-webkit-border-top-right-radius:0}");

        style.type = 'text/css';

        if(style.styleSheet) {
          style.styleSheet.cssText = rules.nodeValue;
        } else {
          style.appendChild(rules);
        }

        document.getElementsByTagName('head')[0].appendChild(style);
      })();

return Tabs;
})(document, parseInt, RightJS);