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 = {
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,
Whitelisted parameters which can be set on construction.
_whitelistedParams: ['defaultAnchorX', 'defaultAnchorY', 'direction', 'contentEl', 'onOpen', 'onClose'],
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
},
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)();
},
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 the open state.
toggle: function() {
this[this.isActive ? 'close' : 'open']();
},
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 the popover. This stops is from positioning itself.
pause: function() {
this.isPaused = true;
this._clearPosition();
},
Resume the popover positioning itself.
resume: function() {
this.isPaused = false;
},
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();
},
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);
}
}
},
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;
},
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);
},
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
});
},
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');
},
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';
},
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);
},
Add event listeners for DOM events.
_addEventListeners: function() {
this.el.addEventListener('click', this._onClickBound);
this.contentEl.addEventListener('click', this._onContentClickBound);
},
Remove event listeners for DOM events..
_removeEventListeners: function() {
this.el.removeEventListener('click', this._onClickBound);
this.contentEl.removeEventListener('click', this._onContentClickBound);
},
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);
},
Remove window event listeners.
_removeWindowEventListeners: function() {
window.removeEventListener('click', this._onWindowClickBound);
window.removeEventListener('resize', this._onWindowResizeBound);
window.removeEventListener('orientationchange', this._onWindowResizeBound);
},
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;
},
Create the caret element.
_createCaretEl: function() {
var el = document.createElement('div');
el.className = 'spark-popover__caret';
this.contentEl.appendChild(el);
return el;
},
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;
}
},
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;
}
},
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();
}
},
When the window is resized, ensure the proper position of the popover.
_onWindowResize: function() {
this._updatePosition();
}
};
Base.exportjQuery(Popover, 'Popover');
return Popover;
}));