Home Reference Source Repository

js/components/saiku/Tabs.jsx

/**
 *   Copyright 2016 OSBI Ltd
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

import React from 'react';
import autoBind from 'react-autobind';
import _ from 'underscore';
import Tab from './Tab';

/**
 * Saiku <Tabs /> component. It requires a createContent function, it is
 * responsible for populate the tab content, whenever the '+' button is pressed.
 * @example
 * function myContent() {
 *   return (<h1>Tab Content</h1>);
 * }
 * <Tabs createContent={myContent}/>
 */
class Tabs extends React.Component {
  constructor(props) {
    super(props);

    this.id = _.uniqueId('tabs_');
    this.tabCounter = 1;
    this.state = {
      tabs: [],
      selectedTab: null
    };

    autoBind(this, 'renderTabButtons', 'renderTabPanels');
    autoBind(this, '_newTab', '_deleteTab', '_selectTab');
  }

  /**
   * Method automatically called when React already built and mounted the
   * component on a dom element. In this case, this method will create the
   * tabs' first tab, filled with the createContent function's content.
   */
  componentDidMount() {
    this._newTab();
  }

  render() {
    return (
      <div>
        <ul className="nav nav-tabs" role="tablist" key={this.id}>
          {this.state.tabs.map(this.renderTabButtons)}
          <li className="add-tab" role="tab">
            <a role="tab" href="#" onClick={this._newTab}>+</a>
          </li>
        </ul>
        {this.state.tabs.map(this.renderTabPanels)}
      </div>
    );
  }

  /**
   * Helper method called for each tab in order to render its button.
   * @param {Object} tab - an object containing tab data
   * @param {string} tab.key - unique tab identifier
   * @param {string} tab.title - text to be displayed on tab's button
   * @param {Object} tab.component - content returned by createContent function
   * @param {number} index - the index of the tab (the render order)
   */
  renderTabButtons(tab, index) {
    return (
      <li
        className={this._isSelected(tab) ? 'active' : ''}
        role="tab"
        key={'tab_button_' + index}
      >
        <a
          role="tab"
          href={'#' + tab.key}
          data-toggle="tab"
          aria-expanded={this._isSelected(tab)}
          aria-controls={tab.key}
          onClick={(event) => this._selectTab(tab, event)}
        >
          <button
            className="close closeTab"
            type="button"
            onClick={(event) => this._deleteTab(tab, event)}
          >
            ×
          </button>
          {tab.title}
        </a>
      </li>
    );
  }

  /**
   * Helper method called for each tab in order to render its content.
   * @param {Object} tab - an object containing tab data
   * @param {string} tab.key - unique tab identifier
   * @param {string} tab.title - text to be displayed on tab's button
   * @param {Object} tab.component - content returned by createContent function
   * @param {number} index - the index of the tab (the render order)
   */
  renderTabPanels(tab, index) {
    return (
      <div className="tab-content" key={tab.key + '_content'}>
        <Tab tabKey={tab.key} isSelected={this._isSelected(tab)}>
          {tab.component}
        </Tab>
      </div>
    );
  }

  /**
   * Utility method to test if a tab is selected of not.
   * @param {Object} tab - an object containing tab data
   * @param {string} tab.key - unique tab identifier
   * @param {string} tab.title - text to be displayed on tab's button
   * @param {Object} tab.component - content returned by createContent function
   */
  _isSelected(tab) {
    return tab.key === this.state.selectedTab;
  }

  /**
   * Method called when the '+' button is pressed. It will instantiate a new tab
   * component, passing the result of createContent function as its child. This
   * method also sets an incremental title to the tab ('Unsaved query(x)').
   */
  _newTab(event) {
    if (event) {
      event.preventDefault();
    }

    let tab = {
      key: _.uniqueId('tab_'),
      title: 'Unsaved query (' + (this.tabCounter) + ')',
      component: this.props.createContent()
    };

    this.state.tabs.push(tab);
    this.tabCounter++;

    this.setState({
      tabs: this.state.tabs
    });

    this._selectTab(tab);
  }

  /**
   * Method called when the user clicks on one tab button. It will set the
   * respective tab as the selected one.
   * @param {Object} tab - an object containing tab data
   * @param {string} tab.key - unique tab identifier
   * @param {string} tab.title - text to be displayed on tab's button
   * @param {Object} tab.component - content returned by createContent function
   */
  _selectTab(tab, event) {
    if (event) {
      event.preventDefault();
    }

    this.setState({selectedTab: tab.key});
  }

  /**
   * Method called when the user clicks on a tab's 'x' button. It will remove
   * this tab and, it it was the selected one, choose another tab to be the
   * new active.
   * @param {Object} tab - an object containing tab data
   * @param {string} tab.key - unique tab identifier
   * @param {string} tab.title - text to be displayed on tab's button
   * @param {Object} tab.component - content returned by createContent function
   */
  _deleteTab(tab, event) {
    if (event) {
      event.preventDefault();
      event.stopPropagation();
    }

    let key = tab.key;
    let tabs = this.state.tabs;

    // Remove the tab
    this.state.tabs = _.without(tabs, _.findWhere(tabs, {key: key}));
    this.setState({tabs: this.state.tabs});

    // If the removed that is the selected, find and select another one
    if (this._isSelected(tab) && (this.state.tabs.length > 0)) {
      _.defer(() => this._selectTab(this.state.tabs[0]));
    }
  }
}

Tabs.propTypes = {
  createContent: React.PropTypes.func.isRequired
};

export default Tabs;