‘use strict’;

!function($) {

/**

* OffCanvas module.
* @module foundation.offcanvas
* @requires foundation.util.keyboard
* @requires foundation.util.mediaQuery
* @requires foundation.util.triggers
* @requires foundation.util.motion
*/

class OffCanvas {

/**
 * Creates a new instance of an off-canvas wrapper.
 * @class
 * @fires OffCanvas#init
 * @param {Object} element - jQuery object to initialize.
 * @param {Object} options - Overrides to the default plugin settings.
 */
constructor(element, options) {
  this.$element = element;
  this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options);
  this.$lastTrigger = $();
  this.$triggers = $();

  this._init();
  this._events();

  Foundation.registerPlugin(this, 'OffCanvas')
  Foundation.Keyboard.register('OffCanvas', {
    'ESCAPE': 'close'
  });

}

/**
 * Initializes the off-canvas wrapper by adding the exit overlay (if needed).
 * @function
 * @private
 */
_init() {
  var id = this.$element.attr('id');

  this.$element.attr('aria-hidden', 'true');

  this.$element.addClass(`is-transition-${this.options.transition}`);

  // Find triggers that affect this element and add aria-expanded to them
  this.$triggers = $(document)
    .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]')
    .attr('aria-expanded', 'false')
    .attr('aria-controls', id);

  // Add an overlay over the content if necessary
  if (this.options.contentOverlay === true) {
    var overlay = document.createElement('div');
    var overlayPosition = $(this.$element).css("position") === 'fixed' ? 'is-overlay-fixed' : 'is-overlay-absolute';
    overlay.setAttribute('class', 'js-off-canvas-overlay ' + overlayPosition);
    this.$overlay = $(overlay);
    if(overlayPosition === 'is-overlay-fixed') {
      $('body').append(this.$overlay);
    } else {
      this.$element.siblings('[data-off-canvas-content]').append(this.$overlay);
    }
  }

  this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className);

  if (this.options.isRevealed === true) {
    this.options.revealOn = this.options.revealOn || this.$element[0].className.match(/(reveal-for-medium|reveal-for-large)/g)[0].split('-')[2];
    this._setMQChecker();
  }
  if (!this.options.transitionTime === true) {
    this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas]')[0]).transitionDuration) * 1000;
  }
}

/**
 * Adds event handlers to the off-canvas wrapper and the exit overlay.
 * @function
 * @private
 */
_events() {
  this.$element.off('.zf.trigger .zf.offcanvas').on({
    'open.zf.trigger': this.open.bind(this),
    'close.zf.trigger': this.close.bind(this),
    'toggle.zf.trigger': this.toggle.bind(this),
    'keydown.zf.offcanvas': this._handleKeyboard.bind(this)
  });

  if (this.options.closeOnClick === true) {
    var $target = this.options.contentOverlay ? this.$overlay : $('[data-off-canvas-content]');
    $target.on({'click.zf.offcanvas': this.close.bind(this)});
  }
}

/**
 * Applies event listener for elements that will reveal at certain breakpoints.
 * @private
 */
_setMQChecker() {
  var _this = this;

  $(window).on('changed.zf.mediaquery', function() {
    if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
      _this.reveal(true);
    } else {
      _this.reveal(false);
    }
  }).one('load.zf.offcanvas', function() {
    if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
      _this.reveal(true);
    }
  });
}

/**
 * Handles the revealing/hiding the off-canvas at breakpoints, not the same as open.
 * @param {Boolean} isRevealed - true if element should be revealed.
 * @function
 */
reveal(isRevealed) {
  var $closer = this.$element.find('[data-close]');
  if (isRevealed) {
    this.close();
    this.isRevealed = true;
    this.$element.attr('aria-hidden', 'false');
    this.$element.off('open.zf.trigger toggle.zf.trigger');
    if ($closer.length) { $closer.hide(); }
  } else {
    this.isRevealed = false;
    this.$element.attr('aria-hidden', 'true');
    this.$element.off('open.zf.trigger toggle.zf.trigger').on({
      'open.zf.trigger': this.open.bind(this),
      'toggle.zf.trigger': this.toggle.bind(this)
    });
    if ($closer.length) {
      $closer.show();
    }
  }
}

/**
 * Stops scrolling of the body when offcanvas is open on mobile Safari and other troublesome browsers.
 * @private
 */
_stopScrolling(event) {
  return false;
}

// Taken and adapted from http://stackoverflow.com/questions/16889447/prevent-full-page-scrolling-ios
// Only really works for y, not sure how to extend to x or if we need to.
_recordScrollable(event) {
  let elem = this; // called from event handler context with this as elem

   // If the element is scrollable (content overflows), then...
  if (elem.scrollHeight !== elem.clientHeight) {
    // If we're at the top, scroll down one pixel to allow scrolling up
    if (elem.scrollTop === 0) {
      elem.scrollTop = 1;
    }
    // If we're at the bottom, scroll up one pixel to allow scrolling down
    if (elem.scrollTop === elem.scrollHeight - elem.clientHeight) {
      elem.scrollTop = elem.scrollHeight - elem.clientHeight - 1;
    }
  }
  elem.allowUp = elem.scrollTop > 0;
  elem.allowDown = elem.scrollTop < (elem.scrollHeight - elem.clientHeight);
  elem.lastY = event.originalEvent.pageY;
}

_stopScrollPropagation(event) {
  let elem = this; // called from event handler context with this as elem
  let up = event.pageY < elem.lastY;
  let down = !up;
  elem.lastY = event.pageY;

  if((up && elem.allowUp) || (down && elem.allowDown)) {
    event.stopPropagation();
  } else {
    event.preventDefault();
  }
}

/**
 * Opens the off-canvas menu.
 * @function
 * @param {Object} event - Event object passed from listener.
 * @param {jQuery} trigger - element that triggered the off-canvas to open.
 * @fires OffCanvas#opened
 */
open(event, trigger) {
  if (this.$element.hasClass('is-open') || this.isRevealed) { return; }
  var _this = this;

  if (trigger) {
    this.$lastTrigger = trigger;
  }

  if (this.options.forceTo === 'top') {
    window.scrollTo(0, 0);
  } else if (this.options.forceTo === 'bottom') {
    window.scrollTo(0,document.body.scrollHeight);
  }

  /**
   * Fires when the off-canvas menu opens.
   * @event OffCanvas#opened
   */
  _this.$element.addClass('is-open')

  this.$triggers.attr('aria-expanded', 'true');
  this.$element.attr('aria-hidden', 'false')
      .trigger('opened.zf.offcanvas');

  // If `contentScroll` is set to false, add class and disable scrolling on touch devices.
  if (this.options.contentScroll === false) {
    $('body').addClass('is-off-canvas-open').on('touchmove', this._stopScrolling);
    this.$element.on('touchstart', this._recordScrollable);
    this.$element.on('touchmove', this._stopScrollPropagation);
  }

  if (this.options.contentOverlay === true) {
    this.$overlay.addClass('is-visible');
  }

  if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
    this.$overlay.addClass('is-closable');
  }

  if (this.options.autoFocus === true) {
    this.$element.one(Foundation.transitionend(this.$element), function() {
      var canvasFocus = _this.$element.find('[data-autofocus]');
      if (canvasFocus.length) {
          canvasFocus.eq(0).focus();
      } else {
          _this.$element.find('a, button').eq(0).focus();
      }
    });
  }

  if (this.options.trapFocus === true) {
    this.$element.siblings('[data-off-canvas-content]').attr('tabindex', '-1');
    Foundation.Keyboard.trapFocus(this.$element);
  }
}

/**
 * Closes the off-canvas menu.
 * @function
 * @param {Function} cb - optional cb to fire after closure.
 * @fires OffCanvas#closed
 */
close(cb) {
  if (!this.$element.hasClass('is-open') || this.isRevealed) { return; }

  var _this = this;

  _this.$element.removeClass('is-open');

  this.$element.attr('aria-hidden', 'true')
    /**
     * Fires when the off-canvas menu opens.
     * @event OffCanvas#closed
     */
      .trigger('closed.zf.offcanvas');

  // If `contentScroll` is set to false, remove class and re-enable scrolling on touch devices.
  if (this.options.contentScroll === false) {
    $('body').removeClass('is-off-canvas-open').off('touchmove', this._stopScrolling);
    this.$element.off('touchstart', this._recordScrollable);
    this.$element.off('touchmove', this._stopScrollPropagation);
  }

  if (this.options.contentOverlay === true) {
    this.$overlay.removeClass('is-visible');
  }

  if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
    this.$overlay.removeClass('is-closable');
  }

  this.$triggers.attr('aria-expanded', 'false');

  if (this.options.trapFocus === true) {
    this.$element.siblings('[data-off-canvas-content]').removeAttr('tabindex');
    Foundation.Keyboard.releaseFocus(this.$element);
  }
}

/**
 * Toggles the off-canvas menu open or closed.
 * @function
 * @param {Object} event - Event object passed from listener.
 * @param {jQuery} trigger - element that triggered the off-canvas to open.
 */
toggle(event, trigger) {
  if (this.$element.hasClass('is-open')) {
    this.close(event, trigger);
  }
  else {
    this.open(event, trigger);
  }
}

/**
 * Handles keyboard input when detected. When the escape key is pressed, the off-canvas menu closes, and focus is restored to the element that opened the menu.
 * @function
 * @private
 */
_handleKeyboard(e) {
  Foundation.Keyboard.handleKey(e, 'OffCanvas', {
    close: () => {
      this.close();
      this.$lastTrigger.focus();
      return true;
    },
    handled: () => {
      e.stopPropagation();
      e.preventDefault();
    }
  });
}

/**
 * Destroys the offcanvas plugin.
 * @function
 */
destroy() {
  this.close();
  this.$element.off('.zf.trigger .zf.offcanvas');
  this.$overlay.off('.zf.offcanvas');

  Foundation.unregisterPlugin(this);
}

}

OffCanvas.defaults = {

/**
 * Allow the user to click outside of the menu to close it.
 * @option
 * @type {boolean}
 * @default true
 */
closeOnClick: true,

/**
 * Adds an overlay on top of `[data-off-canvas-content]`.
 * @option
 * @type {boolean}
 * @default true
 */
contentOverlay: true,

/**
 * Enable/disable scrolling of the main content when an off canvas panel is open.
 * @option
 * @type {boolean}
 * @default true
 */
contentScroll: true,

/**
 * Amount of time in ms the open and close transition requires. If none selected, pulls from body style.
 * @option
 * @type {number}
 * @default 0
 */
transitionTime: 0,

/**
 * Type of transition for the offcanvas menu. Options are 'push', 'detached' or 'slide'.
 * @option
 * @type {string}
 * @default push
 */
transition: 'push',

/**
 * Force the page to scroll to top or bottom on open.
 * @option
 * @type {?string}
 * @default null
 */
forceTo: null,

/**
 * Allow the offcanvas to remain open for certain breakpoints.
 * @option
 * @type {boolean}
 * @default false
 */
isRevealed: false,

/**
 * Breakpoint at which to reveal. JS will use a RegExp to target standard classes, if changing classnames, pass your class with the `revealClass` option.
 * @option
 * @type {?string}
 * @default null
 */
revealOn: null,

/**
 * Force focus to the offcanvas on open. If true, will focus the opening trigger on close.
 * @option
 * @type {boolean}
 * @default true
 */
autoFocus: true,

/**
 * Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`.
 * @option
 * @type {string}
 * @default reveal-for-
 * @todo improve the regex testing for this.
 */
revealClass: 'reveal-for-',

/**
 * Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes.
 * @option
 * @type {boolean}
 * @default false
 */
trapFocus: false

}

// Window exports Foundation.plugin(OffCanvas, ‘OffCanvas’);

}(jQuery);