Popover

function
 Popover() 

Option name Type Description
el Element
params Object

Popover constructor.

var Popover = function(el, params) {

  if (!el) {
    return;
  }

  this._setParams(this.defaults, true);
  this._setParams(params || {});
  this._cacheElements(el);
  this._bindEventListenerCallbacks();
  this._addEventListeners();
};

Popover.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_toggleClass: Base.toggleClass,
_hasClass: Base.hasClass,
_addClass: Base.addClass,
_removeClass: Base.removeClass,
_elementHasParent: Base.elementHasParent,
_getElementMatchingParent: Base.getElementMatchingParent,
_getElementOffset: Base.getElementOffset,
_appendChildren: Base.appendChildren,
remove: Base.remove,

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

_whitelistedParams: ['defaultAnchorX', 'defaultAnchorY', 'direction', 'contentEl', 'onOpen', 'onClose'],

defaults

property
 defaults 

Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.

defaults: {
  el: null,
  toggleEl: null,
  contentEl: null,
  caretEl: null,
  anchorTo: null,
  isActive: false,
  isPaused: false,
  direction: 'bottom',
  defaultAnchorX: 'center',
  defaultAnchorY: 'center',
  closeTimer: null,
  onOpen: null,
  onClose: null,
  _onClickBound: null,
  _onContentClickBound: null,
  _onWindowClickBound: null,
  _onWindowResizeBound: null
},

open

method
 open() 

Option name Type Description
params Object

Optional

Open.

open: function(params) {

  params = params || {};

  // If there is a timer running for the close event, clear it so it doesn't close stuff during open.
  if (this.closeTimer) {
    clearTimeout(this.closeTimer);
    this.closeTimer = null;
  }

  this.anchorTo = params.anchorTo || this.anchorTo;

  this._addWindowEventListeners();
  this.isActive = true;
  this._moveContent();
  this._updatePosition(params);
  this._addClass(this.contentEl, 'animate');
  var e = document.createEvent('Event');
  e.initEvent('spark.visible-children', true, true);
  this.contentEl.dispatchEvent(e);
  this._updateAttributes();

  (params.complete || noop)();
  (this.onOpen || noop)();
},

close

method
 close() 

Option name Type Description
params Object

Optional

Close.

close: function(params) {

  params = params || {};

  // If there is a timer running for the close even, clear it so we don't run close stuff twice.
  if (this.closeTimer) {
    clearTimeout(this.closeTimer);
    this.closeTimer = null;
  }

  this._removeWindowEventListeners();
  this.isActive = false;
  this._updateAttributes();

  this.closeTimer = setTimeout(function() {
    this._clearPosition();
    this._moveContent();
    this._removeClass(this.contentEl, 'animate');
    (params.complete || noop)();
    (this.onClose || noop)();
  }.bind(this), 250);
},

toggle

method
 toggle() 

Toggle the open state.

toggle: function() {
  this[this.isActive ? 'close' : 'open']();
},

setContent

method
 setContent() 

Option name Type Description
content Element, Array, NodeList
params Object

Optional

Set the content. Optionally append instead of replacing.

setContent: function(content, params) {
  params = params || {};
  this._appendChildren(this.contentEl, content.length ? content : [content], !(params.append || false));
},

pause

method
 pause() 

Pause the popover. This stops is from positioning itself.

pause: function() {
  this.isPaused = true;
  this._clearPosition();
},

resume

method
 resume() 

Resume the popover positioning itself.

resume: function() {
  this.isPaused = false;
},

_cacheElements

method
 _cacheElements() 

Option name Type Description
el Element

Store a reference to the tabs list, each tab and each panel.
Set which tab is active, or use the first.

_cacheElements: function(el) {

  // If a content element was already passed, make sure it has a popover content class
  if (this.contentEl) {
    this._addClass(this.contentEl, 'spark-popover__content');
    this._addClass(this.contentEl, 'spark-popover__content--' + this.direction);
  }

  this.el = el;
  this.toggleEl = this.el.querySelector('.spark-popover__toggle, [data-role="toggle"]') || this.el;
  this.contentEl = this.contentEl || this.el.querySelector('[class*="spark-popover__content--"]') || this._createContentEl();
  this.caretEl = this.el.querySelector('.spark-popover__caret') || this._createCaretEl();
  this.isActive = this._hasClass(this.toggleEl, 'popover-active');

  //check if we're on an asp forms page
  var a = document.querySelectorAll('body > form');
  for (var i = 0; i < a.length; i++) {
    if (a[i].contains(this.el)) {
      this.targetForm = a[i];
      break;
    }
  }

  this._determineDirectionAndAnchor();
},

_moveContent

method
 _moveContent() 

Move the content node to be at the root of the page.

_moveContent: function() {
  if (this.targetForm) {
    if (this.isActive) {
      this.targetForm.appendChild(this.contentEl);
    } else if (this.contentEl.parentNode !== this.el && !this.isActive) {
      this.el.appendChild(this.contentEl);
    }
  } else {
    // Move the popover to the root of the body.
    if (this.isActive && (this.contentEl.parentNode !== document.body)) {
      document.body.appendChild(this.contentEl);
    } else if (this.contentEl.parentNode !== this.el && !this.isActive) {
      this.el.appendChild(this.contentEl);
    }
  }
},

_determineDirectionAndAnchor

method
 _determineDirectionAndAnchor() 

Determine the direction of of the popover.

_determineDirectionAndAnchor: function() {

  if (this.contentEl.className.indexOf('popover__content--left') !== -1) {
    return (this.direction = 'left');
  } else if (this.contentEl.className.indexOf('popover__content--right') !== -1) {
    return (this.direction = 'right');
  } else if (this.contentEl.className.indexOf('popover__content--top') !== -1) {
    return (this.direction = 'top');
  }

  this.defaultAnchorX = this.contentEl.getAttribute('data-anchor-x') || this.defaultAnchorX;
  this.defaultAnchorY = this.contentEl.getAttribute('data-anchor-y') || this.defaultAnchorY;
},

_updateAttributes

method
 _updateAttributes() 

Update classes for the open or close state.

_updateAttributes: function() {

  this._toggleClass(this.el, 'popover-active', this.isActive);
  this._toggleClass(this.contentEl, 'popover-active', this.isActive);
  this._toggleClass(this.toggleEl, 'active', this.isActive);
},

_updatePosition

method
 _updatePosition() 

Update the position of the popover.

_updatePosition: function() {

  if (!this.isActive) {
    return;
  }

  if (this.isPaused) {
    return this._clearPosition();
  }

  this._addClass(this.contentEl, 'measure');

  var toggleEl = this.anchorTo || this.toggleEl;
  var caretEl = this.caretEl;
  var toggleOffset = this._getElementOffset(toggleEl);
  var toggleHeight = toggleEl.offsetHeight;
  var toggleWidth = toggleEl.offsetWidth;
  var caretHeight = caretEl.offsetHeight;
  var caretWidth = caretEl.offsetWidth;
  var caretAdjustedWidth = Math.sqrt(Math.pow(caretWidth, 2) + Math.pow(caretHeight, 2)); // Accounts for the 90deg rotation in the CSS
  var contentHeight = this.contentEl.offsetHeight;
  var contentWidth = this.contentEl.offsetWidth;
  var docHeight = document.documentElement.scrollHeight;
  var docWidth = document.documentElement.scrollWidth;
  var xAxis = this.direction === 'bottom' || this.direction === 'top';
  var top = 0;
  var left = 0;
  var anchor = xAxis ? this.defaultAnchorX : this.defaultAnchorY;
  var anchorPos = xAxis ? (toggleWidth / 2) : (toggleHeight / 2);

  // Initial values
  if (this.direction === 'left') {
    top = toggleOffset.top - (contentHeight / 2) + (toggleHeight / 2);
    left = toggleOffset.left - contentWidth;
  } else if (this.direction === 'right') {
    top = toggleOffset.top - (contentHeight / 2) + (toggleHeight / 2);
    left = toggleOffset.left + toggleWidth;
  } else if (this.direction === 'top') {
    top = toggleOffset.top - contentHeight;
    left = toggleOffset.left - (contentWidth / 2) + (toggleWidth / 2);
  } else {
    top = toggleOffset.top + toggleHeight;
    left = toggleOffset.left - (contentWidth / 2) + (toggleWidth / 2);
  }

  // Check boundaries
  if (xAxis) {
    if (left + contentWidth > docWidth) {
      anchor = 'right';
      left = toggleOffset.left + toggleWidth - contentWidth;
      anchorPos = ((toggleWidth - caretAdjustedWidth) / 2 - caretWidth / 2);
    }
    if (left < 0) {
      anchor = 'left';
      left = 0;
      anchorPos = toggleOffset.left + ((toggleWidth - caretAdjustedWidth) / 2 + caretWidth / 2);
    }
  } else {
    if (top + contentHeight > docHeight) {
      anchor = 'bottom';
      anchorPos = anchorPos - (this.caretEl.offsetWidth * 1.5);
      top = toggleOffset.top;
    } else if (top < 0) {
      anchor = 'top';
      top = toggleOffset.top;
    }
  }

  this.contentEl.style.top = top + 'px';
  this.contentEl.style.left = left + 'px';
  this.contentEl.setAttribute('data-anchor', anchor);
  this._removeClass(this.contentEl, 'measure');

  this._updateCaretAnchorPosition({
    anchor: anchor,
    anchorPos: anchorPos
  });
},

_clearPosition

method
 _clearPosition() 

Clear position styling.

_clearPosition: function() {
  this.contentEl.removeAttribute('style');
  this.contentEl.removeAttribute('data-anchor-x');
  this.contentEl.removeAttribute('data-anchor-y');
  this.caretEl.removeAttribute('style');
},

_updateCaretAnchorPosition

method
 _updateCaretAnchorPosition() 

Update the position of the caret to be centered on the toggle element.

_updateCaretAnchorPosition: function(params) {

  if (!this.caretEl) {
    return;
  }

  this.caretEl.style.left = '';
  this.caretEl.style.right = '';
  this.caretEl.style.top = '';
  this.caretEl.style.bottom = '';
  this.caretEl.style[params.anchor] = params.anchorPos + 'px';
},

_bindEventListenerCallbacks

method
 _bindEventListenerCallbacks() 

Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.

_bindEventListenerCallbacks: function() {
  this._onClickBound = this._onClick.bind(this);
  this._onContentClickBound = this._onContentClick.bind(this);
  this._onWindowClickBound = this._onWindowClick.bind(this);
  this._onWindowResizeBound = this._onWindowResize.bind(this);
},

_addEventListeners

method
 _addEventListeners() 

Add event listeners for DOM events.

_addEventListeners: function() {
  this.el.addEventListener('click', this._onClickBound);
  this.contentEl.addEventListener('click', this._onContentClickBound);
},

_removeEventListeners

method
 _removeEventListeners() 

Remove event listeners for DOM events..

_removeEventListeners: function() {
  this.el.removeEventListener('click', this._onClickBound);
  this.contentEl.removeEventListener('click', this._onContentClickBound);
},

_addWindowEventListeners

method
 _addWindowEventListeners() 

Add event listeners to the window.

_addWindowEventListeners: function() {
  this._removeWindowEventListeners();
  window.addEventListener('click', this._onWindowClickBound);
  window.addEventListener('resize', this._onWindowResizeBound);
  window.addEventListener('orientationchange', this._onWindowResizeBound);
},

_removeWindowEventListeners

method
 _removeWindowEventListeners() 

Remove window event listeners.

_removeWindowEventListeners: function() {
  window.removeEventListener('click', this._onWindowClickBound);
  window.removeEventListener('resize', this._onWindowResizeBound);
  window.removeEventListener('orientationchange', this._onWindowResizeBound);
},

_createContentEl

method
 _createContentEl() 

Create a content element.

_createContentEl: function() {
  var el = document.createElement('div');
  this._addClass(el, 'spark-popover__content');
  this._addClass(el, 'spark-popover__content--' + this.direction);
  el.setAttribute('role', 'tooltip');
  return el;
},

_createCaretEl

method
 _createCaretEl() 

Create the caret element.

_createCaretEl: function() {
  var el = document.createElement('div');
  el.className = 'spark-popover__caret';
  this.contentEl.appendChild(el);
  return el;
},

_onClick

method
 _onClick() 

Option name Type Description
e Object

When we are clicked, toggle the popover-active state.

_onClick: function(e) {

  // If this is the toggle element, toggle.
  if (e.target === this.toggleEl || this._elementHasParent(e.target, this.toggleEl)) {
    e.preventDefault();
    this.toggle();
    return;
  }
},

_onContentClick

method
 _onContentClick() 

Option name Type Description
e Object

When the toggle is clicked, close if it's a link. If it's content, don't do anything but stop
the event from bubbling.

_onContentClick: function(e) {

  // If this is a link, close.
  if (this._getElementMatchingParent(e.target, '.spark-popover__list-link', this.contentEl) || this._getElementMatchingParent(e.target, '.spark-popover__close', this.contentEl)) {
    this.close();
    return;
  }
},

_onWindowClick

method
 _onWindowClick() 

Option name Type Description
e Objec

When the window is clicked and it's not part of the popover, close the popover.

_onWindowClick: function(e) {
  if (e.target !== this.el && !this._elementHasParent(e.target, this.el) && !this._elementHasParent(e.target, this.contentEl)) {
    this.close();
  }
},

_onWindowResize

method
 _onWindowResize() 

When the window is resized, ensure the proper position of the popover.

_onWindowResize: function() {
  this._updatePosition();
}
  };

  Base.exportjQuery(Popover, 'Popover');

  return Popover;
}));