APIs

Show:
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';
import { A } from '@ember/array';
import $ from "jquery"

/**
  Component's CSS-classes names.
  JSON-object containing string constants with CSS-classes names related to component's .hbs markup elements.

  @property {Object} flexberryClassNames
  @property {String} flexberryClassNames.prefix Component's CSS-class names prefix ('flexberry-tab-bar').
  @property {String} flexberryClassNames.wrapper Component's wrapping <div> CSS-class name ('flexberry-tab-bar').
  @property {String} flexberryClassNames.tab Component's inner <input type="checkbox"> CSS-class name ('flexberry-tab-bar-tab').
  @property {String} flexberryClassNames.tabIcon Component's inner <label> CSS-class name ('flexberry-tab-bar-tab-icon').
  @readonly
  @static

  @for FlexberryTabBarComponent
*/
const flexberryClassNamesPrefix = 'flexberry-tab-bar';
const flexberryClassNames = {
  prefix: flexberryClassNamesPrefix,
  wrapper: flexberryClassNamesPrefix,
  tab: flexberryClassNamesPrefix + '-tab',
  tabIcon: flexberryClassNamesPrefix + '-tab-icon'
};

/**
 * FlexberryTabBarComponent
 * Component to display semantic ui tabs
 * @extends Component
 */
export default Component.extend({
  classNames: ['ui', 'tabular', 'menu', flexberryClassNamesPrefix],

  classNameBindings: ['isOverflowedTabs:overflowed'],

  /**
    Reference to component's CSS-classes names.
    Must be also a component's instance property to be available from component's .hbs template.
  */
  flexberryClassNames,

  /**
   * Contains items to be displayed in tab bar
   * @property items
   * @type {Array}
   * @default null
   * @example items: [{selector: 'tab1', caption: 'Tab one', active: true }, {selector: 'tab2', caption: 'Tab two'}]
   * @desc the first item with active=true will be set as active, others ignored
   */
  items: null,

  /**
   * Contains items that will be displayed in tab bar
   * @property tabs
   * @type {Array}
   */
  tabs: computed('items.{[],@each.active}', function () {
    let active = false;
    let items = this.get('items') || A();
    let result = A();

    items.forEach((item) => {
      let itemIsActive = get(item, 'active');
      if (itemIsActive) {
        if (!active) {
          active = true;

          let itemClass = get(item, 'class') || '';
          let regEx = /\sactive(\s|$)/;
          if (!regEx.test(itemClass)) {
            itemClass += ' active';
          }

          set(item, 'class', itemClass);
          this.set('prevTab', item.selector);
          $.tab('change tab', item.selector);
        }
      }

      set(item, 'active', false);

      if (get(item, 'iconClass')) {
        set(item, '_hasIcon', true);
      } else {
        set(item, '_hasIcon', false);
      }

      result.pushObject(item);
    });

    return result;
  }),

  /**
   * Contains name of previous data-tab
   * @property prevTab
   */
  prevTab: undefined,

  /**
   * String with dropdown selector for working with jQuery
   * @property dropdownDomString
   */
  navDropdownDomString: '.ui.compact.pointing.top.right.dropdown.link.item',

  isOverflowedTabs: false,

  /**
   * Checks if sum of tabs width is greater than tab container.
   * If true - dropdown becomes visible.
   * If false - dropdown dissapears.
   * @method setDropdownVisibility
   */
  setNavDropdownVisibility: function () {
    const tabContainer = document.querySelector(
      '.ui.tabular.menu.flexberry-tab-bar'
    );
    const tab = document.querySelector('.flexberry-tab-bar-tab.tab.item');

    if (tab != null && tab.clientWidth * this.items.length > tabContainer.clientWidth) {
      this.$(this.navDropdownDomString).show();
    } else {
      this.$(this.navDropdownDomString).hide();
    }
  },

  /**
   * Enables scroll wheel for dragscroll.
   * Based on code by @miorel + @pieterv of Facebook.
	 * github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
   * @method normalizeWheel
   */
  normalizeWheel: function (event) {
    const pixelStep = 10;
    const lineHeight = 40;
    const pageHeight = 800;
    let sX = 0;
    let sY = 0;
    let pX = 0;
    let pY = 0;

    // Legacy.
    if ('detail' in event) {
      sY = event.detail;
    } else if ('wheelDelta' in event) {
      sY = event.wheelDelta / -120;
    } else if ('wheelDeltaY' in event) {
      sY = event.wheelDeltaY / -120;
    }

    if ('wheelDeltaX' in event) {
      sX = event.wheelDeltaX / -120;
    }

    // Side scrolling on FF with DOMMouseScroll.
    if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
      sX = sY;
      sY = 0;
    }

    // Calculate.
    pX = sX * pixelStep;
    pY = sY * pixelStep;

    if ('deltaY' in event) {
      pY = event.deltaY;
    }

    if ('deltaX' in event) {
      pX = event.deltaX;
    }

    if ((pX || pY) && event.deltaMode) {
      if (event.deltaMode === 1) {
        pX *= lineHeight;
        pY *= lineHeight;
      } else {
        pX *= pageHeight;
        pY *= pageHeight;
      }
    }

    // Fallback if spin cannot be determined.
    if (pX && !sX) {
      sX = pX < 1 ? -1 : 1;
    }

    if (pY && !sY) {
      sY = pY < 1 ? -1 : 1;
    }

    // Return.
    return {
      spinX: sX,
      spinY: sY,
      pixelX: pX,
      pixelY: pY,
    };
  },

  actions: {
    /**
        Handles tab 'click' action.
        @method actions.change
        @param {String} currentTab describes currently clicked tab
        @param {Object} event [jQuery event object](http://api.jquery.com/category/events/event-object/)
    */
    change(currentTab, event) {
      let prevTab = this.get('prevTab');
      let tabName = currentTab;
      let changed = false;

      if (prevTab !== tabName) {
        this.set('prevTab', tabName);
        changed = true;
      }

      // if data-tab stays the same - disable it
      if (!changed) {
        this.set('prevTab', undefined);
        set(currentTab, 'active', false);
      }

      //if data-tab changed but there was not prev one
      else if (changed && !prevTab) {
        changed = false;
      }

      let e = {
        tabName: tabName,
        prevTab: prevTab,
        changed: changed,
        originalEvent: event,
      };

      this.get('change', e);
    },
  },

  /**
      Initializes component's DOM-related properties.
    */
  didInsertElement() {
    this._super(...arguments);

    // initialize semantic ui tabs
    this.$('.item').tab();

    if (this.get('isOverflowedTabs')) {
      // Dragscroll inplementation for tabs
      const slider = document.querySelector('.dragscroll');
      let isDown = false;
      let startX;
      let scrollLeft;

      slider.addEventListener('mousedown', (e) => {
        isDown = true;
        slider.classList.add('active');
        startX = e.pageX - slider.offsetLeft;
        scrollLeft = slider.scrollLeft;
      });

      slider.addEventListener('mouseleave', () => {
        isDown = false;
        slider.classList.remove('active');
      });

      slider.addEventListener('mouseup', () => {
        isDown = false;
        slider.classList.remove('active');
      });

      slider.addEventListener('mousemove', (e) => {
        if (!isDown) {
          return false;
        } else {
          e.preventDefault();
          const x = e.pageX - slider.offsetLeft;
          const walk = (x - startX) * 1.5;
          slider.scrollLeft = scrollLeft - walk;
        }
      });

      slider.addEventListener('wheel', (e) => {

        // Prevent default.
        e.preventDefault();
        e.stopPropagation();

        // Stop link scroll.
        this.$('body').stop();

        // Calculate delta, direction.
        const n = this.normalizeWheel(e);
        const x = n.pixelX !== 0 ? n.pixelX : n.pixelY;
        const delta = Math.min(Math.abs(x), 150);
        const direction = x > 0 ? 1 : -1;

        // Scroll page.
        this.$('.dragscroll').scrollLeft(this.$('.dragscroll').scrollLeft() + delta * direction);
      });

      // Dropdown visibility implementation
      window.addEventListener('resize', () => {
        this.setNavDropdownVisibility();
      });

      this.setNavDropdownVisibility();
    }
  },

  /**
    Handles DOM-related component's properties after each render.
  */
  didRender() {
    this._super(...arguments);

    // Initialize possibly added new tabs.
    this.$('.item').tab();

    if (this.get('isOverflowedTabs')) {
      // Inititalize semantic ui dropdown (hidden by default)
      this.$(this.navDropdownDomString).dropdown({
        transition: 'drop',
        action: 'activate',
        onChange(newTab) {
          $.tab('change tab', newTab);
        },
      });
    }
  },

  /**
    Deinitializes component's DOM-related properties.
  */
  willDestroyElement() {
    this._super(...arguments);

    // destroy semantic ui tabs
    this.$('.item').tab('destroy');
  },

  /**
    Component's action invoking when tab was clicked and it's 'active' state changed.
    @method sendingActions.change
    @param {Object} e Action's event object.
    @param {String} e.tabName Name of clicked tab
    @param {Boolean} e.changed Flag: whether tab was changed. True if yes, False if not, Null if there is no active tab
    @param {Object} e.originalEvent [jQuery event object](http://api.jquery.com/category/events/event-object/)
    which describes inner input's 'change' event.
  */
});