takahashiakira
9/30/2015 - 7:41 AM

Accordion Class v1.4.0

Accordion Class v1.4.0

Accordion Class

CSS3アニメーションに対応したアコーディオンです。optionでアコーディオンのネストに対応。 対応ブラウザはモダンブラウザ(IE10は以上、IE8,9はアニメーション以外は動く)
※スタイルは別途用意してください。

/**
 * Accordion Class Description v1.4.0
 * @fileoverview CSS3アニメーションに対応したアコーディオンです。optionでアコーディオンのネストに対応
 *    対応ブラウザはモダンブラウザ(IE10は以上、IE8,9はアニメーション以外は動く)
 */
var Accordion = (function () {
  /** @constructor */
  var constructor = function () {
    this.$introBlock = {};
    this.$detailBlock = {};
    this.$openTrigger = {};
    this.$closeTrigger = {};
    this.introBlockParentClass = {};
    this.isLoading = false; /** ボタンを押せる状態かどうか */
    this.isInnerDetail = false; /** 開く要素が初期要素を中にあるかどうか */
    this.isNested = false; /** 入れ子のアコーディオンかどうか */
    this.introHeight = 0;
    this.speed = 0; /** optionsで指定可 */
    this.childAccordion = {
      introBlock : '.js-accordion-introBlock',
      detailBlock :  '.js-accordion-detailBlock',
      openTrigger : '.js-accordion-openTrigger',
      closeTrigger : '.js-accordion-closeTrigger'
    };
  };
  var proto = constructor.prototype;
  /**
   * Entry point
   * @param {Object} el - 全ての要素を含んだラッパーDOM要素
   * @param {Object} options - 任意でoptionの指定
   *   {Number} options.speed - アニメーションスピードの指定
   *   {Object} options.childAccordion - アコーディオンのネストが必要な場合、ここで子要素{introBlock, detailBlock, openTrigger, closeTrigger}のクラス名を直接指定
   */
  proto.set = function (el, options) {
    this.setOptions(options);
    if(el){
      this.$el = $(el);
      this.setEl();
    }else{
      throw new Error('el is Required');
    }
    this.setEl();
    this.setStyle();
    this.setEvents();
    return this;
  };
  proto.setOptions = function (options) {
    if(options){
      //** optionsを一つでも指定した場合 */
      this.speed = options.speed || this.speed;
      if(options.childAccordion){
        this.isNested = true;
        this.introBlockParentClass = this.childAccordion.introBlock;
        this.childAccordion = options.childAccordion;
      }
    }
  };
  proto.setEl = function () {
    this.$introBlock = this.$el.find(this.childAccordion.introBlock);
    this.$detailBlock = this.$el.find(this.childAccordion.detailBlock);
    this.$openTrigger = this.$el.find(this.childAccordion.openTrigger);
    this.$closeTrigger = this.$el.find(this.childAccordion.closeTrigger);
    this.introHeight = this.$introBlock.outerHeight(true);
    if(this.isNested){
      this.$introBlockParent = this.$el.closest(this.introBlockParentClass);
    }
    return this;
  };
  proto.setStyle = function () {
    var that = this;
    if(this.introHeight <= 0){
      /** introがタブで初期表示が非表示の場合、表示されている先頭タブの要素の高さに合わせる */
      /** iOSでheight()が取れないのでcss('max-height')を取る */
      var maxH = this.$introBlock.closest('.hide').parent().find(this.childAccordion.introBlock).eq(0).css('max-height');
      if(!this.isNested){
        this.introHeight = parseInt(maxH, 10);
      }else{
        /** Accordionが入れ子になっている場合は親のintroBlockの高さを取得  */
        maxH = this.$introBlockParent.eq(0).css('max-height');
        this.introHeight = parseInt(maxH, 10);
      }
    }
    var defaultStyle = {
      'min-height': that.introHeight +'px',
      'max-height': that.introHeight +'px',
      'overflow-y': 'hidden'
    };
    var setTransition = {'transition': 'all ' +(this.speed)/1000 +'s ease-in-out'};
    var isInnerDetail = this.$introBlock.find(this.childAccordion.detailBlock).length > 0;
    if(isInnerDetail){
      /** Introの中にdetailが存在する場合 */
      this.isInnerDetail = true;
      this.$introBlock.css(defaultStyle);
      if(this.speed > 0){
        this.$introBlock.css(setTransition);
      }
    }else{
      /** Introとdetailが別々に存在する場合 */
      this.$detailBlock.css(defaultStyle);
      if(this.speed > 0){
        this.$detailBlock.css(setTransition);
      }
    }
    return this;
  };
  proto.setEvents = function () {
    var that = this;
    this.$openTrigger.click(function (e) {
      if(!this.isLoading){
        e.preventDefault();
        that.open();
      }
      return this;
    });
    this.$closeTrigger.click(function (e) {
      if(!this.isLoading){
        e.preventDefault();
        that.close();
      }
      return this;
    });
    return this;
  };
  proto.open = function () {
    var that = this;
    var defaultOpen = function () {
      that.$detailBlock.removeClass('hide');
      if(!that.isInnerDetail){
        that.$introBlock.addClass('hide');
      }else{
        that.$openTrigger.addClass('hide');
        that.$closeTrigger.removeClass('hide');
      }
      return this;
    };
    var setMaxHeight = function () {
      var maxHeight = 0;
      if(!that.isInnerDetail){
        /** introとdetailが分離している場合 */
        var detailHeight = 0;
        var detailMarginPadding = that.$detailBlock.outerHeight(true) -that.$detailBlock.height();
        that.$detailBlock.children().each(function() {
          detailHeight = detailHeight +$(this).outerHeight(true);
        });
        maxHeight = detailHeight +detailMarginPadding;
        that.$detailBlock.css('max-height', maxHeight +'px');
      }else{
        /** detailがinnerの中にある場合 */
        var introHeight = 0;
        var introMarginPadding = that.$introBlock.outerHeight(true) -that.$introBlock.height();
        that.$introBlock.children().each(function() {
          introHeight = introHeight +$(this).outerHeight(true);
        });
        maxHeight = introHeight +introMarginPadding;
        that.$introBlock.css('max-height', maxHeight +'px');
        if(that.isNested){
          /** 入れ子のアコーディオンの場合 */
          var pareantMaxHeight = that.$introBlockParent.outerHeight(true) +maxHeight;
          that.$introBlockParent.css('max-height', pareantMaxHeight +'px');
        }
      }
      return this;
    };
    if(this.speed > 0){
      /** speedを指定した場合はCSS3でスライドダウンアニメーション */
      this.isLoading = true;
      defaultOpen();
      setTimeout(function () {
        setMaxHeight();
        setTimeout(function(){
          that.isLoading = false;
        }, that.speed);
      }, 0); /** 1msずらさないとアニメーションできない */
    }else{
      defaultOpen();
      setMaxHeight();
    }
    return this;
  };
  proto.close = function () {
    var that = this;
    var defaultClose = function () {
      that.$detailBlock.addClass('hide');
      if(!that.isInnerDetail){
        that.$introBlock.removeClass('hide');
      }else{
        that.$openTrigger.removeClass('hide');
        that.$closeTrigger.addClass('hide');
      }
      return this;
    };
    var setMaxHeight = function () {
      if(!that.isInnerDetail){
        that.$detailBlock.css({'max-height': that.introHeight});
      }else{
        that.$introBlock.css({'max-height': that.introHeight});
      }
    };
    if(this.speed > 0){
      /** speedを指定した場合はCSS3でスライドアップアニメーション */
      this.isLoading = true;
      setMaxHeight();
      setTimeout(function(){
        that.isLoading = false;
        defaultClose();
      }, that.speed);
    }else{
      setMaxHeight();
      defaultClose();
    }
    return this;
  };
  return constructor;
})();

Usage

HTML

<div id="js-accordion">
  <div class="js-accordion-introBlock">
    <div>introBlock</div>
    <div class="js-accordion-openTrigger">open</div>
  </div>
  <div class="js-accordion-detailBlock hide">
    <div>detailBlock</div>
    <div class="js-accordion-closeTrigger">close</div>
    <div>content<br>content<br>content<br>content<br>content<br>content<br>content<br>content<br></div>
  </div>
</div>

<!-- Anmation & Nested -->
<div id="js-nestedAccordionParent">
  <div class="js-accordion-introBlock">
    <div>introBlock</div>
    <div class="js-accordion-openTrigger">open</div>
    <div class="js-accordion-detailBlock hide">
      <div>detailBlock</div>
      <div class="js-accordion-closeTrigger">close</div>
      <div>content<br>content<br>content<br>content<br>content<br>content<br>content<br>content<br></div>
      <div id="js-nestedAccordionChild">
        <div class="js-nestedAccordion-introBlock">
          <div>introBlock</div>
          <div class="js-nestedAccordion-openTrigger">open</div>
          <div class="js-nestedAccordion-detailBlock hide">
            <div>detailBlock</div>
            <div class="js-nestedAccordion-closeTrigger">close</div>
            <div>content<br>content<br>content<br>content<br>content<br>content<br>content<br>content<br></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

JS

// Default
var accordionDefault = new Accordion();
accordionDefault.set('#js-accordion');

// Anmation & Nested
var nestedAccordionParent = new Accordion();
nestedAccordionParent.set('#js-nestedAccordionParent', { speed: 300 });
var nestedAccordionChild = new Accordion();
var childAccordion = {
  introBlock : '.js-nestedAccordion-introBlock',
  detailBlock :  '.js-nestedAccordion-detailBlock',
  openTrigger : '.js-nestedAccordion-openTrigger',
  closeTrigger : '.js-nestedAccordion-closeTrigger'
};
nestedAccordionChild.set('#js-nestedAccordionChild', { speed: 300, childAccordion: childAccordion });