drawerjs.js

/**
 * A navigation drawer for mobile and web applications.
 * @class
 *
 * @example <caption>The Drawer basic template looks like:</caption>
 * <main id="wrapper">
 *   <!-- the Drawer -->
 *   <aside id="drawerjs" class="drawerjs">
 *     <!-- your awesome navigations here... -->
 *     <h1>Drawerjs</h1>
 *   </aside>
 *   <!-- the Holder -->
 *   <section id="drawerjs-holder" class="drawerjs-holder">
 *     <h2>Site content</h2>
 *   </section>
 * </main>
 *
 * @example <caption>Instance the Drawer:</caption>
 * var drawer = new Drawerjs();
 */
class Drawerjs {

  /**
   * Create a Drawer.
   * @constructor
   * @param  {Object} options - An object containing key:value that representing the current Drawer.
   */
  constructor(options) {

    /**
     * The Drawer options.
     * @type {Object}
     * @property {String} align='left'                    - The Drawer position (left/right).
     * @property {Boolean} compact=false                  - The Drawer compact mode.
     * @property {Boolean} fixed=false                    - The Drawer fixed mode.
     * @property {(Boolean|Object)} forcePos=false        - CSS to force the Drawer positions and dimensions. Only support `top`, `right`, `bottom`, `left`, `width`, `height`.
     * @property {String} holder='#drawerjs-holder'       - The Holder element selector, usually targeted to site/app main content. Set to `false` if you haven't a holder, the Drawer will refer to document body.
     * @property {String} holderClass='drawerjs-holder'   - The css class name that will added to holder selector automatically.
     * @property {Boolean} nested=false                   - Indicator for nested Drawer. Set to `true` to use nested Drawer.
     * @property {Boolean} open=false                     - Open/close the Drawer.
     * @property {Boolean} pinned=false                   - If `true`, it's transform the Drawer to push/pull navigation when open/close the Drawer.
     * @property {String} selector='#drawerjs'            - The Drawer element selector.
     * @property {String} selectorClass='drawerjs'        - The css class name that will be added to the Drawer and used for css prefix classes.
     * @property {(Number|String)} width='200px'          - The Drawer width for default size. Support for any css units like `rem`, `em`, or `%`.
     * @property {(Number|String)} compactWidth='60px'    - The Drawer width for compact size. Support for any css units like `rem`, `em`, or `%`.
     * @property {(Boolean|Object)} useCustom=false       - Use custom skins on the Drawer and the Holder. Both are support `backgroundColor` and `color` properties.
     * @property {Boolean} holderBehavior=true            - If Activated, the Holder will translate the content when screen width < `toggleScreen` instead of using content space (You may want to disable this behavior when work with multiple drawer).
     * @property {(Number|String)} toggleScreen='992px'   - The Drawer use it to initialize the default options, depend on screen sizes. When screen width < `toggleScreen` the Drawer will used default options for mobile.
     * @property {Boolean} backdrop=true                  - Enable/disable the Drawer backdrop. When this property is `false` the unpinned drawer will not automatically closed on outside click.
     * @example
     * var options = {
     *   align: 'left',
     *   compact: true,
     *   fixed: true,
     *   useCustom: {
     *     drawer: {
     *       backgroundColor: '#fff',
     *       color: '#000'
     *     },
     *     holder: {
     *       backgroundColor: '#fff',
     *       color: '#000'
     *     }
     *   }
     * };
     *
     * var drawer = new Drawerjs(options);
     */
    this.options = {
      align: 'left',
      compact: false,
      fixed: false,
      forcePos: false,
      holder: '#drawerjs-holder',
      holderClass: 'drawerjs-holder',
      nested: false,
      open: false,
      pinned: false,
      selector: '#drawerjs',
      selectorClass: 'drawerjs',
      width: '200px',
      compactWidth: '60px',
      useCustom: false,
      // be careful when edit below property
      holderBehavior: true,
      toggleScreen: '992px',
      backdrop: true
    }

    options = options || {}

    // validate width, mush have valid css unit
    options.width = options.width ? this._validateUnit(options.width) : this.options.width
    options.compactWidth = options.compactWidth ? this._validateUnit(options.compactWidth) : this.options.compactWidth

    // force options depend on screen size (toggle at min-width: 992px by default)
    if (window.matchMedia) {
      this.options.fixed = !matchMedia(`only screen and (min-width: ${this._validateUnit(this.options.toggleScreen)})`).matches
      this.options.open = matchMedia(`only screen and (min-width: ${this._validateUnit(this.options.toggleScreen)})`).matches
      this.options.pinned = matchMedia(`only screen and (min-width: ${this._validateUnit(this.options.toggleScreen)})`).matches
    }

    // mixed default and custom options
    this.options = this._extend({}, this.options, options)

    /**
     * The Drawer element.
     * @type {Element}
     */
    this.selector = typeof this.options.selector === 'object' ? this.options.selector : document.querySelector(this.options.selector)

    /**
     * The Drawer holder.
     * If option set to `false`, the Holder will be refer to document body. Its usefull for multiple drawer instance.
     * @type {Element}
     */
    this.holder = (!this.options.holder)
      ? document.querySelector('body')
      : document.querySelector(this.options.holder)

    /**
     * The Drawer wrapper.
     * @type {Element}
     */
    this.wrapper = this.holder.parentNode

    /**
     * The Drawer uniq ID.
     * @type {String}
     */
    this.hash = Math.random().toString(36).substring(7)

    /**
     * Lists of feature classes that will be added to the Drawer depend to current options.
     * Used selectorClass for prefix.
     * @type {Object}
     */
    this.classes = {
      alignRight: this.options.selectorClass + '-has-right',
      compact: this.options.selectorClass + '-has-compact',
      fixed: this.options.selectorClass + '-has-fixed',
      open: this.options.selectorClass + '-has-open',
      close: this.options.selectorClass + '-has-close',
      slideIn: this.options.selectorClass + '-has-slideIn',
      pinned: this.options.selectorClass + '-has-pinned',
      wrapper: this.options.selectorClass + '-wrapper'
    }

    // Initialization
    this.init()
  }

  /** Private methods */
  /**
   * Listen on document when the page is ready.
   * @private
   * @param  {Function} handler - The callback function when page is ready.
   * @return {void}
   */
  _onReady(handler) {
    if(document.readyState != 'loading') {
      handler()
    } else {
      document.addEventListener('DOMContentLoaded', handler, false)
    }
  }

  /**
   * Merge the contents of two or more objects together into the first object.
   * @private
   * @param  {Object} obj - An object containing additional properties to merge in.
   * @return {Object} The merged object.
   */
  _extend(obj) {
    obj = obj || {}
    const args = arguments
    for (let i = 1; i < args.length; i++) {
      if (!args[i]) continue
      for (let key in args[i]) {
        if (args[i].hasOwnProperty(key))
          obj[key] = args[i][key]
      }
    }
    return obj
  }

  /**
   * Validate unit value to support Drawer rules.
   * @private
   * @param  {(Number|String)} unit - Unit value to validate.
   * @return {Boolean} Has valid unit.
   */
  _validateUnit(unit) {
    const expr = /(^[\d]+((px)|(vw)|(r?em)|(\%))$)|^(initial|inherit|auto|0)$/i
    return expr.test(unit) ? unit : `${/[^\.\sa-z]\d*\.?\d*/g.exec(unit)[0]}px`
  }

  /**
   * Attach an event to Drawer selector.
   * @private
   * @param  {String} type - The name of the event (case-insensitive).
   * @param  {(Boolean|Number|String|Array|Object)} data - The custom data that will be added to event.
   * @return {void}
   */
  _emit(type, data) {
    let e
    if (document.createEvent) {
      e = document.createEvent('Event')
      e.initEvent(type, true, true)
    } else {
      e = document.createEventObject()
      e.eventType = type
    }
    e.eventName = type
    e.data = data || this
    // attach event to selector
    document.createEvent
      ? this.selector.dispatchEvent(e)
      : this.selector.fireEvent('on' + type, e)
  }

  /**
   * Registers the specified listener on the element.
   * @private
   * @param  {Element} el - The target element.
   * @param  {String} type - The name of the event.
   * @param  {Function} handler - The callback function when event type is fired.
   * @return {void}
   */
  _on(el, type, handler) {
    let types = type.split(' ')
    for (let i = 0; i < types.length; i++) {
      el[window.addEventListener ? 'addEventListener' : 'attachEvent']( window.addEventListener ? types[i] : `on${types[i]}` , handler, false)
    }
  }

  /**
   * Removes the event listener previously registered with [_on()]{@link Drawerjs#_on} method.
   * @private
   * @param  {Element} el - The target element.
   * @param  {String} type - The name of the event.
   * @param  {Function} handler - The callback function when event type is fired.
   * @return {void}
   */
  _off(el, type, handler) {
    let types = type.split(' ')
    for (let i = 0; i < types.length; i++) {
      el[window.removeEventListener ? 'removeEventListener' : 'detachEvent']( window.removeEventListener ? types[i] : `on${types[i]}` , handler, false)
    }
  }

  /**
   * Adds one or more class names to the target element.
   * @private
   * @param {Element} el - The target element.
   * @param {String} className - Specifies one or more class names to be added.
   * @return {void}
   */
  _addClass(el, className) {
    let classes = className.split(' ')
    for (let i = 0; i < classes.length; i++) {
      if (el.classList) el.classList.add(classes[i])
      else el.classes[i] += ' ' + classes[i]
    }
  }

  /**
   * Removes one or more class names to the target element.
   * @private
   * @param {Element} el - The target element.
   * @param {String} className - Specifies one or more class names to be added.
   * @return {void}
   */
  _removeClass(el, className) {
    let classes = className.split(' ')
    for (let i = 0; i < classes.length; i++) {
      if (el.classList) el.classList.remove(classes[i])
      else el.classes[i] = el.classes[i].replace(new RegExp('(^|\\b)' + classes[i].split(' ').join('|') + '(\\b|$)', 'gi'), ' ')
    }
  }

  /**
   * Get the current Drawer width.
   * @private
   * @param  {Boolean} isCompact - The current value of compact value.
   * @return {String} The current Drawer width.
   */
  _drawerWidth(isCompact) {
    return isCompact ? this.options.compactWidth : this.options.width
  }

  /**
   * Apply the current Drawer width.
   * @private
   * @return {void}
   */
  _renderWidth() {
    this.selector.style.width = this._drawerWidth(this.isCompact())
  }

  /**
   * Clear the Holder space.
   * @private
   * @return {void}
   */
  _clearSpace() {
    // for holder = body
    this.holder.style.paddingRight = 0
    this.holder.style.paddingLeft = 0
    // for holder = specific element
    this.holder.style.marginRight = 0
    this.holder.style.marginLeft = 0
    // for screen width < toggleScreen
    this.holder.style.transform = ''
  }

  /**
   * Give space to the Holder.
   * @private
   * @return {void}
   */
  _holderSpace() {
    let opts = this.options
    let space = !opts.holder ? 'padding' : 'margin'
    // clear space
    this._clearSpace()
    // holder behavior: translate holder instead of adding space on screen width < toggleScreen
    if (opts.holderBehavior && !this.isCompact() && !matchMedia(`only screen and (min-width: ${this._validateUnit(this.options.toggleScreen)})`).matches) {
      if (opts.pinned && opts.open) {
        if (opts.align === 'right') {
          this.holder.style.transform = `translateX(-${this._drawerWidth(this.isCompact())})`
        } else if(opts.align === 'left') {
          this.holder.style.transform = `translateX(${this._drawerWidth(this.isCompact())})`
        }
      }
    } else {
      // add space by align
      if (opts.pinned && opts.open) {
        if (opts.align === 'right') {
          this.holder.style[`${space}Right`] = this._drawerWidth(this.isCompact())
        } else if(opts.align === 'left') {
          this.holder.style[`${space}Left`] = this._drawerWidth(this.isCompact())
        }
      }
    }
  }

  /**
   * Add/remove the current Drawer backdrop depend with pinned option.
   * @private
   * @return {void}
   */
  _backdrop() {
    const self = this
    const hashId = `backdrop-${this.hash}`
    let opts = this.options
    let body = document.querySelector('body')
    let el = document.createElement('div')

    el.setAttribute('id', hashId)
    this._addClass(el, `${opts.selectorClass}-backdrop`)

    if (!this.isPinned() && opts.backdrop) {
      let backdropEl = document.querySelector(`#${hashId}`)
      if (!body.contains(backdropEl)) {
        body.appendChild(el)
        this._on(el, 'click', () => {
          self.close()
        })
      }
      if (this.isOpen()) {
        document.querySelector(`#${hashId}`).style.display = 'block'
      }
    } else {
      let backdropEl = document.querySelector(`#${hashId}`)
      if (body.contains(backdropEl)) backdropEl.parentNode.removeChild(backdropEl)
    }
  }

  /**
   * A transition that is used when open the Drawer.
   * @private
   * @return {void}
   */
  _transitionIn() {
    this._addClass(this.selector, this.classes.slideIn)
    this.selector.style.width = this._drawerWidth(this.isCompact())
    // transitionIn when forcePos.height is exist
    const pos = this.options.forcePos
    if (pos.hasOwnProperty('height')) this.selector.style.height = pos.height
  }

  /**
   * A transition that is used when close the Drawer.
   * @private
   * @return {void}
   */
  _transitionOut() {
    this._removeClass(this.selector, this.classes.slideIn)
    this.selector.style.width = 0
    // transitionIn when forcePos.height is exist
    const pos = this.options.forcePos
    if (pos.hasOwnProperty('height')) this.selector.style.height = 0
  }

  /**
   * Get the correct transitionend event for any browser.
   * @private
   * @param  {Element} el - The target element.
   * @return {String} The name of the transitionend.
   */
  _transitionEnd(el) {
    const transEndEventNames = {
      'WebkitTransition' : 'webkitTransitionEnd',
      'MozTransition'    : 'transitionend',
      'OTransition'      : 'oTransitionEnd otransitionend',
      'transition'       : 'transitionend'
    }
    for (let name in transEndEventNames) {
      if (el.style[name] !== undefined) {
        return transEndEventNames[name]
      }
    }
  }

  /**
   * Set the Drawer wrapper background color same as the Drawer.
   * @private
   * @return {void}
   */
  _addFakeHeight() {
    const backgroundColor = window.getComputedStyle(this.selector).backgroundColor
    this.wrapper.style.backgroundColor = backgroundColor
  }

  /**
   * Remove the Drawer wrapper background color given by [_addFakeHeight()]{@link Drawerjs#_addFakeHeight} method.
   * @private
   * @return {void}
   */
  _removeFakeHeight() {
    this.wrapper.style.backgroundColor = ''
  }


  /** Public methods */
  /**
   * The first process that called after constructs the Drawer instance.
   * @public
   * @fires Drawerjs#drawer:init
   * @return {void}
   */
  init() {
    const self = this
    let opts = this.options

    this._addClass(this.selector, opts.selectorClass)
    this._addClass(this.holder, opts.holderClass)
    this._addClass(this.wrapper, this.classes.wrapper)

    // give default width
    this._renderWidth()
    // hold transition, hide transition on init
    this._addClass(this.selector, `${opts.selectorClass}-hold-transition`)
    this._addClass(this.holder, `${opts.selectorClass}-hold-transition`)
    // trigger transition
    this._on(this.selector, this._transitionEnd(this.selector), () => {
      if(self.isOpen() || self.isCompact()) {
        self.selector.style.overflow = ''
      } else if(!self.isOpen()) {
        self.selector.style.opacity = '0'
        self.selector.style.visibility = 'hidden'
      }
    })
    // stop anaimateEnd bubling event
    if (this.selector.hasChildNodes()) {
      let children = this.selector.children
      Array.prototype.forEach.call(children, el => {
        self._on(el, self._transitionEnd(el), e => {
          e.stopPropagation()
        })
      })
    }

    // is nested
    if (opts.nested) {
      this.wrapper.style.position = 'relative'
    }

    // implement placement feature
    this.align(opts.align)
    // implement compact feature
    this.compact(opts.compact)
    // implement fixed feature
    this.fixed(opts.fixed)
    // force css position
    this.forcePos(opts.forcePos)
    // implement pinned feature
    this.pinned(opts.pinned)
    // use custom
    this.custom(opts.useCustom)
    // implement show/hide feature
    this.isOpen() ? this.open() : this.close()

    // on ready state
    this._onReady(() => {
      self.selector.style.overflow = ''
      // resume transition
      setTimeout(() => {
        self._removeClass(self.selector, `${opts.selectorClass}-hold-transition`)
        self._removeClass(self.holder, `${opts.selectorClass}-hold-transition`)
      }, 150)

      /**
       * This event is fired when the Drawer has completed init.
       *
       * @event Drawerjs#drawer:init
       * @type {Object}
       * @property {Object} data - The Drawerjs data instance.
       *
       * @example
       * document.querySelector('#drawerjs').addEventListener('drawer:init', function(e) {
       *   console.log(e.data);
       * });
       * @example <caption>Or using jQuery:</caption>
       * $('#drawerjs').on('drawer:init', function() {
       *   console.log('fired on drawer:init!!');
       * });
       */
      self._emit('drawer:init')
    })
  }

  /**
   * Set the Drawer position to `left` or `right`.
   * @public
   * @fires Drawerjs#drawer:align
   * @param  {String} position - The position that will be set to the Drawer.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.align('right');
   */
  align(position) {
    const classes = this.classes

    if (position === 'right') {
      this._addClass(this.selector, classes.alignRight)
    } else {
      this._removeClass(this.selector, classes.alignRight)
    }

    this.options.align = position

    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has changed position.
     *
     * @event Drawerjs#drawer:align
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:align', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:align', function() {
     *   console.log('fired on drawer:align!!');
     * });
     */
    this._emit('drawer:align')

    return this
  }

  /**
   * Determine whether the Drawer is currently compact.
   * @public
   * @return {Boolean} is compact.
   *
   * @example
   * var isCompact = drawer.isCompact();
   */
  isCompact() {
    return this.options.compact
  }

  /**
   * Toggle the Drawer compact mode.
   * @public
   * @fires Drawerjs#drawer:compact
   * @param  {Boolean} isCompact - The compact mode.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.compact(true);
   */
  compact(isCompact) {
    const method = (isCompact) ? '_addClass': '_removeClass'
    const classes = this.classes

    this[method](this.selector, classes.compact)
    this.selector.style.overflow = 'hidden'

    this.options.compact = isCompact

    // draw current width
    this._renderWidth()
    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has completed toggle compact mode.
     *
     * @event Drawerjs#drawer:compact
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:compact', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:compact', function() {
     *   console.log('fired on drawer:compact!!');
     * });
     */
    this._emit('drawer:compact')

    return this
  }

  /**
   * Determine whether the Drawer is currently fixed.
   * @public
   * @return {Boolean} Is fixed.
   *
   * @example
   * var isFixed = drawer.isFixed();
   */
  isFixed() {
    return this.options.fixed
  }

  /**
   * Toggle the Drawer fixed mode.
   * @public
   * @fires Drawerjs#drawer:fixed
   * @param  {Boolean} isFixed - The fixed mode.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.fixed(true);
   */
  fixed(isFixed) {
    const method = (isFixed) ? '_addClass': '_removeClass'
    const classes = this.classes

    // just added fake synchronous backgroundColor to wrapper
    isFixed ? this._removeFakeHeight() : this._addFakeHeight()
    this[method](this.selector, classes.fixed)
    // force redraw/repaint to fixed -webkit- overflow render
    this.selector.style.display = 'none'
    this.selector.offsetHeight
    this.selector.style.display = ''

    this.options.fixed = isFixed

    /**
     * This event is fired when the Drawer has completed toggle fixed mode.
     *
     * @event Drawerjs#drawer:fixed
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:fixed', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:fixed', function() {
     *   console.log('fired on drawer:fixed!!');
     * });
     */
    this._emit('drawer:fixed')

    return this
  }

  /**
   * Whether the Drawer is currently open.
   * @public
   * @return {Boolean} Is open.
   *
   * @example
   * var isOpen = drawer.isOpen();
   */
  isOpen() {
    return this.options.open
  }

  /**
   * Open/show the Drawer.
   * @public
   * @fires Drawerjs#drawer:open
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.open();
   */
  open() {
    const classes = this.classes

    this._transitionIn()
    this._addClass(this.selector, classes.open)
    this._removeClass(this.selector, classes.close)
    this.selector.style.opacity = ''
    this.selector.style.visibility = ''
    // show backdrop
    if (!this.isPinned() && this.options.backdrop) {
      const backdrop = document.querySelector(`#backdrop-${this.hash}`)
      backdrop.style.display = 'block'
    }

    this.options.open = true

    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has open.
     *
     * @event Drawerjs#drawer:open
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:open', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:open', function() {
     *   console.log('fired on drawer:open!!');
     * });
     */
    this._emit('drawer:open')

    return this
  }

  /**
   * Close/hide the Drawer.
   * @public
   * @fires Drawerjs#drawer:close
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.close();
   */
  close() {
    const classes = this.classes

    this._transitionOut()
    this._addClass(this.selector, classes.close)
    this._removeClass(this.selector, classes.open)
    this.selector.style.overflow = 'hidden'
    // hide backdrop
    if (!this.isPinned() && this.options.backdrop) {
      const backdrop = document.querySelector(`#backdrop-${this.hash}`)
      backdrop.style.display = ''
    }

    this.options.open = false

    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has close.
     *
     * @event Drawerjs#drawer:close
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:close', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:close', function() {
     *   console.log('fired on drawer:close!!');
     * });
     */
    this._emit('drawer:close')

    return this
  }

  /**
   * Toggle open/close the Drawer. This method trigger both `open` and `close` event.
   * @public
   * @fires Drawerjs#drawer:open
   * @fires Drawerjs#drawer:close
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.toggle();
   */
  toggle() {
    const method = (!this.isOpen()) ? 'open' : 'close'

    this[method]()

    return this
  }

  /**
   * Determine whether the Drawer is currently pinned.
   * @public
   * @return {Boolean} Is pinned.
   *
   * @example
   * var isPinned = drawer.isPinned();
   */
  isPinned() {
    return this.options.pinned
  }

  /**
   * Toggle the Drawer pinned mode.
   * @public
   * @fires Drawerjs#drawer:pinned
   * @param  {Boolean} isPinned - The pinned mode.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.pinned(true);
   */
  pinned(isPinned) {
    const method = (isPinned) ? '_addClass': '_removeClass'
    const classes = this.classes

    this[method](this.selector, classes.pinned)

    this.options.pinned = isPinned

    // add/remove backdrop
    this._backdrop()
    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has completed toggle pinned.
     *
     * @event Drawerjs#drawer:pinned
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:pinned', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:pinned', function() {
     *   console.log('fired on drawer:pinned!!');
     * });
     */
    this._emit('drawer:pinned')

    return this
  }


  /** Behavior methods */
  /**
   * Get the default Drawer width.
   * @public
   * @return {String} CSS unit value.
   *
   * @example
   * var width = drawer.getWidth();
   */
  getWidth() {
    return this.options.width
  }

  /**
   * Set the default Drawer width.
   * @public
   * @fires Drawerjs#drawer:changeWidth
   * @param {(Number|String)} width - The Drawer width.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.setWidth('300px');
   */
  setWidth(width) {
    this.options.width = this._validateUnit(width)

    // refresh width
    this._renderWidth()
    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has changed width.
     *
     * @event Drawerjs#drawer:changeWidth
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:changeWidth', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:changeWidth', function() {
     *   console.log('fired on drawer:changeWidth!!');
     * });
     */
    this._emit('drawer:changeWidth')

    return this
  }

  /**
   * Get the compact Drawer width.
   * @public
   * @return {String} CSS unit value.
   *
   * @example
   * var compactWidth = drawer.getCompactWidth();
   */
  getCompactWidth() {
    return this.options.compactWidth
  }

  /**
   * Set the compact Drawer width.
   * @public
   * @fires Drawerjs#drawer:changeCompactWidth
   * @param {(Number|String)} width - The Drawer width.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.setCompactWidth(100);
   */
  setCompactWidth(width) {
    this.options.compactWidth = this._validateUnit(width)

    // refresh width
    this._renderWidth()
    // refresh holder space
    this._holderSpace()

    /**
     * This event is fired when the Drawer has changed compact width.
     *
     * @event Drawerjs#drawer:changeCompactWidth
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:changeCompactWidth', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:changeCompactWidth', function() {
     *   console.log('fired on drawer:changeCompactWidth!!');
     * });
     */
    this._emit('drawer:changeCompactWidth')

    return this
  }

  /**
   * Adds css property to force Drawer positions and dimentions.
   * @public
   * @fires Drawerjs#drawer:forcePos
   * @param {Boolean|Object} pos   - Only support CSS properties below:
   * @param {(Number|String)} pos.top       - CSS top property (default unit is `px`).
   * @param {(Number|String)} pos.right     - CSS right property (default unit is `px`).
   * @param {(Number|String)} pos.bottom    - CSS bottom property (default unit is `px`).
   * @param {(Number|String)} pos.left      - CSS left property (default unit is `px`).
   * @param {(Number|String)} pos.width     - CSS width property (default unit is `px`).
   * @param {(Number|String)} pos.height    - CSS height property (default unit is `px`).
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.forcePos({
   *   top: '60px'
   * });
   */
  forcePos(pos) {
    const self = this

    if (typeof pos === 'object') {
      for(let key in pos) {
        let expr = /^(top|left|right|bottom|width|height)$/g
        if (expr.test(key)) {
          self.selector.style.minHeight = 0
          self.selector.style[key] = self._validateUnit(pos[key])
        }
        else {
          console.warn(`This method doesn't support for css ${key}. Only support for top|right|bottom|left|width|height.`)
        }
      }
      // special prop
      if(pos.hasOwnProperty('height')) this.selector.style.minHeight = 0
      if(pos.hasOwnProperty('width')) this.setWidth(pos['width'])
      if((pos.hasOwnProperty('top') || pos.hasOwnProperty('bottom')) && pos.hasOwnProperty('right') && pos.hasOwnProperty('left')) this.selector.style.transition = 'height 150ms linear'
      if(pos.hasOwnProperty('right') && pos.hasOwnProperty('left')) {
        this.selector.style.maxWidth = '100%'
        this.selector.style.transform = 'translateX(0)'
      }
    } else {
      // reset force position
      this.selector.style.maxWidth = ''
      this.selector.style.minHeight = ''
      this.selector.style.transform = ''
      this.selector.style.transition = ''
      this.selector.style.top = ''
      this.selector.style.right = ''
      this.selector.style.bottom = ''
      this.selector.style.left = ''
    }

    /**
     * This event is fired when the Drawer has force position.
     *
     * @event Drawerjs#drawer:forcePos
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:forcePos', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:forcePos', function() {
     *   console.log('fired on drawer:forcePos!!');
     * });
     */
    this._emit('drawer:forcePos')

    return this
  }

  /**
   * Custom using css property for the Drawer and the Holder.
   * @public
   * @fires Drawerjs#drawer:custom
   * @param {(Boolean|Object)} useCustom - The Drawer custom property.
   * @param {String} useCustom.drawer.backgroundColor - The Drawer backgroundColor property.
   * @param {String} useCustom.drawer.color           - The Drawer color property.
   * @param {String} useCustom.holder.backgroundColor - The Holder backgroundColor property.
   * @param {String} useCustom.holder.color           - The Holder color property.
   * @return {Object} The Drawer instance.
   *
   * @example
   * drawer.custom({
   *   drawer: {
   *     backgroundColor: '#f7f7f7',
   *     color: '#333'
   *   }
   * });
   */
  custom(useCustom) {
    const self = this

    if (useCustom.hasOwnProperty('drawer') || useCustom.hasOwnProperty('holder')) {
      for(let elem in useCustom) {
        for(let prop in useCustom[elem]) {
          let regElem = /^(drawer|holder)$/g
          let regProp = /^(backgroundColor|color)$/g
          let target = elem === 'drawer' ? 'selector' : 'holder'
          if (regElem.test(elem) && regProp.test(prop)) self[target].style[prop] = useCustom[elem][prop]
          else console.warn(`Unsupported property in useCustom: ${elem} or ${prop} is not a supported property.`)
        }
      }
      // re-touch the wrapper
      this._addFakeHeight()
    } else {
      this.selector.style.backgroundColor = ''
      this.selector.style.color = ''
      this.holder.style.backgroundColor = ''
      this.holder.style.color = ''
      // re-touch the wrapper
      this._removeFakeHeight()
    }

    this.options.useCustom = useCustom

    /**
     * This event is fired when the Drawer has customized.
     *
     * @event Drawerjs#drawer:custom
     * @type {Object}
     * @property {Object} data - The Drawerjs data instance.
     *
     * @example
     * document.querySelector('#drawerjs').addEventListener('drawer:custom', function(e) {
     *   console.log(e.data);
     * });
     * @example <caption>Or using jQuery:</caption>
     * $('#drawerjs').on('drawer:custom', function() {
     *   console.log('fired on drawer:custom!!');
     * });
     */
    this._emit('drawer:custom')

    return this
  }
}

export default Drawerjs