uktrade/invest-ui

View on GitHub
core/static/js/dit.class.modal.js

Summary

Maintainability
A
1 hr
Test Coverage

/* Class: Modal
 * -------------------------
 * Create an area to use as popup/modal/lightbox effect.
 *
 * REQUIRES:
 * jquery
 * dit.js
 * dit.responsive.js
 *
 **/
(function($, utils, classes) {

  var ARIA_EXPANDED = "aria-expanded";
  var CSS_CLASS_CLOSE_BUTTON = "close";
  var CSS_CLASS_CONTAINER = "Modal-Container"
  var CSS_CLASS_CONTENT = "content";
  var CSS_CLASS_OPEN = "open";
  var CSS_CLASS_OVERLAY = "Modal-Overlay";

  /* Constructor
   * @options (Object) Allow some configurations
   **/
  classes.Modal = Modal;
  function Modal($container, options) {
    var modal = this;
    var config = $.extend({
      $activators: $(), // (optional) Element(s) to control the Modal
      closeOnBuild: true, // Whether intial Modal view is open or closed
      overlay: true,  // Whether it has an overlay or not
      closeButtonId: '', // Option to add custom close button id
      onClose: function() {} // (optional) Callback called on modal close
    }, options || {});

    // If no arguments, likely just being inherited
    if (arguments.length) {
      // Create the required elements
      if(config.overlay) {
        this.$overlay = Modal.createOverlay();
        Modal.bindResponsiveOverlaySizeListener.call(this);
      }

      this.$closeButton = Modal.createCloseButton(config.closeButtonId);
      this.$content = Modal.createContent();
      this.$container = Modal.enhanceModalContainer($container);
      this.onClose = config.onClose;

      // Add elements to DOM
      Modal.appendElements.call(this, config.overlay);

      // Add events
      Modal.bindCloseEvents.call(this);
      Modal.bindActivators.call(this, config.$activators);

      // Initial state
      if (config.closeOnBuild) {
        this.close();
      }
      else {
        this.open();
      }
    }
  }

  Modal.createOverlay = function() {
    var $overlay = $(document.createElement("div"));
    $overlay.addClass(CSS_CLASS_OVERLAY);
    return $overlay;
  }

  Modal.createCloseButton = function(closeButtonId) {
    var $button = $(document.createElement("button"));
    $button.text("Close");
    $button.addClass(CSS_CLASS_CLOSE_BUTTON);
    if (closeButtonId) $button.attr('id', closeButtonId);
    return $button;
  }

  Modal.createContent = function() {
    var $content = $(document.createElement("div"));
    $content.addClass(CSS_CLASS_CONTENT);
    return $content;
  }

  Modal.findFirstFocusElement = function($container) {
    return $container.find("video, a, button, input[type=submit], select").eq(0);
  }

  Modal.findLastFocusElement = function($container) {
    return $container.find("video, a, button, input[type=submit], select").last();
  }

  Modal.enhanceModalContainer = function($container) {
    $container.addClass(CSS_CLASS_CONTAINER);
    return $container;
  }

  Modal.appendElements = function(overlay) {
    this.$container.append(this.$content);
    this.$container.append(this.$closeButton);

    if (overlay) {
      $(document.body).append(this.$overlay);
    }
    $(document.body).append(this.$container);
  }

  // Handles open actions including whether additioal
  // ability to focus and remember activator if using
  // the keyboard for navigation.
  Modal.activate = function(activator, event) {
    this.activator = activator;
    this.open();
    switch(event.which) {
      case 1: // mouse
        this.shouldReturnFocusToActivator = false;
        break;
      case 13: // Enter
        this.shouldReturnFocusToActivator = true;
        this.focus();
        break;
    }
  }

  // Handles close including whether additional
  // ability to refocus on original activator
  // (e.g. if using keyboard for navigaiton).
  Modal.deactivate = function() {
    if(this.shouldReturnFocusToActivator) {
      this.activator.focus();
    }

    this.close();
    this.activator = null;
  }

  Modal.bindCloseEvents = function() {
    var self = this;

    self.$container.on("keydown", function(e) {
      // Close on Esc
      if(e.which === 27) {
        Modal.deactivate.call(self);
      }
    });

    self.$closeButton.on("click", function(e) {
      // Close on click
      Modal.deactivate.call(self);
      e.preventDefault();
    });

    if (self.$overlay && self.$overlay.length) {
      self.$overlay.on("click", function(e) {
        Modal.deactivate.call(self);
      });
    }
  }

  Modal.bindKeyboardFocusEvents = function() {
    var self = this;
    // Loop around to last element when pressing
    // shift+tab on first focusable element
    self.$firstFocusElement.off("keydown.modalfocus");
    self.$firstFocusElement.on("keydown.modalfocus", function(e) {
      if (e.shiftKey && e.which === 9) {
        e.preventDefault();
        self.$lastFocusElement.focus();
      }
    });
    // Loop around to first element when
    // pressing tab on last element
    self.$lastFocusElement.off("keydown.modalfocus");
    self.$lastFocusElement.on("keydown.modalfocus", function(e) {
      if (!e.shiftKey && e.which === 9) {
        e.preventDefault();
        self.$firstFocusElement.focus();
      }
    });
  }

  Modal.bindActivators = function($activators) {
    var self = this;
    $activators.on("click keydown", function(e) {
      // Click or Enter
      if(e.which === 1 || e.which === 13) {
        Modal.activate.call(self, this, e);
        e.preventDefault();
      }
    });
  }

  Modal.bindResponsiveOverlaySizeListener = function() {
    var self = this;
    // Resets the overlay height (once) on scroll because document
    // height changes with responsive resizing and the browser
    // needs a delay to redraw elements. Alternative was to have
    // a rubbish setTimeout with arbitrary delay.
    $(document.body).on(dit.responsive.reset, function(e, mode) {
      $(window).off("scroll.ModalOverlayResizer");
      $(window).one("scroll.ModalOverlayResizer", function() {
        Modal.setOverlayHeight(self.$overlay);
      });
    });
  }

  Modal.setOverlayHeight = function($overlay) {
    $overlay.get(0).style.height = ""; // Clear it first
    $overlay.height($(document).height());
  }

  Modal.prototype = {};
  Modal.prototype.close = function() {
    var self = this;
    self.$container.fadeOut(50, function () {
      self.$container.attr(ARIA_EXPANDED, false);
      self.$container.removeClass(CSS_CLASS_OPEN);
      self.onClose();
    });

    if (self.$overlay && self.$overlay.length) {
      self.$overlay.fadeOut(150);
    }

  }

  Modal.prototype.open = function() {
    var self = this;
    var top;
    if (window.pageYOffset) {
      top = window.pageYOffset;
    }
    else {
      top = document.documentElement.scrollTop;
    }

    self.$container.css("top", top + "px");
    self.$container.addClass(CSS_CLASS_OPEN);
    self.$container.fadeIn(250, function () {
      self.$container.attr(ARIA_EXPANDED, true);
    });

    if (self.$overlay && self.$overlay.length) {
      Modal.setOverlayHeight(self.$overlay);
      self.$overlay.fadeIn(0);
    }
  }

  Modal.prototype.setContent = function(content) {
    var self = this;
    self.$content.empty();
    self.$content.append(content);
    self.$firstFocusElement = Modal.findFirstFocusElement(self.$container);
    self.$lastFocusElement = Modal.findLastFocusElement(self.$container);
    Modal.bindKeyboardFocusEvents.call(self);
  }

  // Tries to add focus to the first found element allowed with natural focus ability.
  Modal.prototype.focus = function() {
    var self = this;
    self.$firstFocusElement.focus();
  }


})(jQuery, dit.utils, dit.classes);