/*
* webui popover plugin - v1.2.0
* A lightWeight popover plugin with jquery ,enchance the popover plugin of bootstrap with some awesome new features. It works well with bootstrap ,but bootstrap is not necessary!
* https://github.com/sandywalker/webui-popover
*
* Made by Sandy Duan
* Under MIT License
*/
;
(function($, window, document, undefined) {
'use strict';
// Create the defaults once
var pluginName = 'webuiPopover';
var pluginClass = 'webui-popover';
var pluginType = 'webui.popover';
var defaults = {
placement: 'auto',
width: 'auto',
height: 'auto',
trigger: 'click', //hover,click,sticky,manual
style: '',
delay: {
show: null,
hide: null
},
async: {
before: null, //function(that, xhr){}
success: null //function(that, xhr){}
},
cache: true,
multi: false,
arrow: true,
title: '',
content: '',
closeable: false,
padding: true,
url: '',
type: 'html',
animation: null,
template: '
',
backdrop: false,
dismissible: true,
onShow: null,
onHide: null,
abortXHR: true,
autoHide: false
};
var popovers = [];
var backdrop = $('');
var _globalIdSeed = 0;
var _isBodyEventHandled = false;
var _offsetOut = -2000; // the value offset out of the screen
var $document = $(document);
// The actual plugin constructor
function WebuiPopover(element, options) {
this.$element = $(element);
if (options) {
if ($.type(options.delay) === 'string' || $.type(options.delay) === 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}; // bc break fix
}
}
this.options = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this._targetclick = false;
this.init();
popovers.push(this.$element);
}
WebuiPopover.prototype = {
//init webui popover
init: function() {
//init the event handlers
if (this.getTrigger() === 'click') {
this.$element.off('click touchend').on('click touchend', $.proxy(this.toggle, this));
} else if (this.getTrigger() === 'hover') {
this.$element
.off('mouseenter mouseleave click')
.on('mouseenter', $.proxy(this.mouseenterHandler, this))
.on('mouseleave', $.proxy(this.mouseleaveHandler, this));
}
this._poped = false;
this._inited = true;
this._opened = false;
this._idSeed = _globalIdSeed;
if (this.options.backdrop) {
backdrop.appendTo(document.body).hide();
}
_globalIdSeed++;
if (this.getTrigger() === 'sticky') {
this.show();
}
},
/* api methods and actions */
destroy: function() {
var index = -1;
for (var i = 0; i < popovers.length; i++) {
if (popovers[i] === this.$element) {
index = i;
break;
}
}
popovers.splice(index, 1);
this.hide();
this.$element.data('plugin_' + pluginName, null);
if (this.getTrigger() === 'click') {
this.$element.off('click');
} else if (this.getTrigger() === 'hover') {
this.$element.off('mouseenter mouseleave');
}
if (this.$target) {
this.$target.remove();
}
},
/*
param: force boolean value, if value is true then force hide the popover
param: event dom event,
*/
hide: function(force, event) {
if (!force && this.getTrigger() === 'sticky') {
return;
}
if (!this._opened) {
return;
}
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (this.xhr && this.options.abortXHR === true) {
this.xhr.abort();
this.xhr = null;
}
var e = $.Event('hide.' + pluginType);
this.$element.trigger(e, [this.$target]);
if (this.$target) {
this.$target.removeClass('in').addClass(this.getHideAnimation());
var that = this;
setTimeout(function() {
that.$target.hide();
}, 300);
}
if (this.options.backdrop) {
backdrop.hide();
}
this._opened = false;
this.$element.trigger('hidden.' + pluginType, [this.$target]);
if (this.options.onHide) {
this.options.onHide(this.$target);
}
},
resetAutoHide: function() {
var that = this;
var autoHide = that.getAutoHide();
if (autoHide) {
if (that.autoHideHandler) {
clearTimeout(that.autoHideHandler);
}
that.autoHideHandler = setTimeout(function() {
that.hide();
}, autoHide);
}
},
toggle: function(e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
this[this.getTarget().hasClass('in') ? 'hide' : 'show']();
},
hideAll: function() {
for (var i = 0; i < popovers.length; i++) {
popovers[i].webuiPopover('hide');
}
$document.trigger('hiddenAll.' + pluginType);
},
/*core method ,show popover */
show: function() {
var
$target = this.getTarget().removeClass().addClass(pluginClass).addClass(this._customTargetClass);
if (!this.options.multi) {
this.hideAll();
}
if (this._opened) {
return;
}
// use cache by default, if not cache setted , reInit the contents
if (!this.getCache() || !this._poped || this.content === '') {
this.content = '';
this.setTitle(this.getTitle());
if (!this.options.closeable) {
$target.find('.close').off('click').remove();
}
if (!this.isAsync()) {
this.setContent(this.getContent());
} else {
this.setContentASync(this.options.content);
}
$target.show();
}
this.displayContent();
if (this.options.onShow) {
this.options.onShow($target);
}
this.bindBodyEvents();
if (this.options.backdrop) {
backdrop.show();
}
this._opened = true;
this.resetAutoHide();
},
displayContent: function() {
var
//element postion
elementPos = this.getElementPosition(),
//target postion
$target = this.getTarget().removeClass().addClass(pluginClass).addClass(this._customTargetClass),
//target content
$targetContent = this.getContentElement(),
//target Width
targetWidth = $target[0].offsetWidth,
//target Height
targetHeight = $target[0].offsetHeight,
//placement
placement = 'bottom',
e = $.Event('show.' + pluginType);
//if (this.hasContent()){
this.$element.trigger(e, [$target]);
//}
if (this.options.width !== 'auto') {
$target.width(this.options.width);
}
if (this.options.height !== 'auto') {
$targetContent.height(this.options.height);
}
if (this.options.style) {
this.$target.addClass(pluginClass + '-' + this.options.style);
}
//init the popover and insert into the document body
if (!this.options.arrow) {
$target.find('.arrow').remove();
}
$target.detach().css({
top: _offsetOut,
left: _offsetOut,
display: 'block'
});
if (this.getAnimation()) {
$target.addClass(this.getAnimation());
}
$target.appendTo(document.body);
targetWidth = $target[0].offsetWidth;
targetHeight = $target[0].offsetHeight;
placement = this.getPlacement(elementPos);
//This line is just for compatible with knockout custom binding
this.$element.trigger('added.' + pluginType);
this.initTargetEvents();
var postionInfo = this.getTargetPositin(elementPos, placement, targetWidth, targetHeight);
this.$target.css(postionInfo.position).addClass(placement).addClass('in');
if (this.options.type === 'iframe') {
var $iframe = $target.find('iframe');
$iframe.width($target.width()).height($iframe.parent().height());
}
if (!this.options.padding) {
if (this.options.height !== 'auto') {
$targetContent.css('height', $targetContent.outerHeight());
}
this.$target.addClass('webui-no-padding');
}
if (!this.options.arrow) {
this.$target.css({
'margin': 0
});
}
if (this.options.arrow) {
var $arrow = this.$target.find('.arrow');
$arrow.removeAttr('style');
if (postionInfo.arrowOffset) {
//hide the arrow if offset is negative
if (postionInfo.arrowOffset.left === -1 || postionInfo.arrowOffset.top === -1) {
$arrow.hide();
} else {
$arrow.css(postionInfo.arrowOffset);
}
}
}
this._poped = true;
this.$element.trigger('shown.' + pluginType, [this.$target]);
},
isTargetLoaded: function() {
return this.getTarget().find('i.glyphicon-refresh').length === 0;
},
/*getter setters */
getTriggerElement: function() {
return this.$element;
},
getTarget: function() {
if (!this.$target) {
var id = pluginName + this._idSeed;
this.$target = $(this.options.template)
.attr('id', id)
.data('trigger-element', this.getTriggerElement());
this._customTargetClass = this.$target.attr('class') !== pluginClass ? this.$target.attr('class') : null;
this.getTriggerElement().attr('data-target', id);
}
return this.$target;
},
getTitleElement: function() {
return this.getTarget().find('.' + pluginClass + '-title');
},
getContentElement: function() {
return this.getTarget().find('.' + pluginClass + '-content');
},
getTitle: function() {
return this.$element.attr('data-title') || this.options.title || this.$element.attr('title');
},
getUrl: function() {
return this.$element.attr('data-url') || this.options.url;
},
getAutoHide: function() {
return this.$element.attr('data-auto-hide') || this.options.autoHide;
},
getCache: function() {
var dataAttr = this.$element.attr('data-cache');
if (typeof(dataAttr) !== 'undefined') {
switch (dataAttr.toLowerCase()) {
case 'true':
case 'yes':
case '1':
return true;
case 'false':
case 'no':
case '0':
return false;
}
}
return this.options.cache;
},
getTrigger: function() {
return this.$element.attr('data-trigger') || this.options.trigger;
},
getDelayShow: function() {
var dataAttr = this.$element.attr('data-delay-show');
if (typeof(dataAttr) !== 'undefined') {
return dataAttr;
}
return this.options.delay.show === 0 ? 0 : this.options.delay.show || 100;
},
getHideDelay: function() {
var dataAttr = this.$element.attr('data-delay-hide');
if (typeof(dataAttr) !== 'undefined') {
return dataAttr;
}
return this.options.delay.hide === 0 ? 0 : this.options.delay.hide || 100;
},
getAnimation: function() {
var dataAttr = this.$element.attr('data-animation');
return dataAttr || this.options.animation;
},
getHideAnimation: function() {
var ani = this.getAnimation();
return ani ? ani + '-out' : 'out';
},
setTitle: function(title) {
var $titleEl = this.getTitleElement();
if (title) {
$titleEl.html(title);
} else {
$titleEl.remove();
}
},
hasContent: function() {
return this.getContent();
},
getContent: function() {
if (this.getUrl()) {
if (this.options.type === 'iframe') {
this.content = $('').attr('src', this.getUrl());
}
} else if (!this.content) {
var content = '';
if ($.isFunction(this.options.content)) {
content = this.options.content.apply(this.$element[0], [this]);
} else {
content = this.options.content;
}
this.content = this.$element.attr('data-content') || content;
if (!this.content) {
var $next = this.$element.next();
if ($next && $next.hasClass(pluginClass)) {
this.content = $next.html();
$next.remove();
}
}
}
return this.content;
},
setContent: function(content) {
var $target = this.getTarget();
this.getContentElement().html(content);
this.$target = $target;
},
isAsync: function() {
return this.options.type === 'async';
},
setContentASync: function(content) {
var that = this;
if (this.xhr) {
return;
}
this.xhr = $.ajax({
url: this.getUrl(),
type: 'GET',
cache: this.getCache(),
beforeSend: function(xhr) {
if (that.options.async.before) {
that.options.async.before(that, xhr);
}
},
success: function(data) {
that.bindBodyEvents();
if (content && $.isFunction(content)) {
that.content = content.apply(that.$element[0], [data]);
} else {
that.content = data;
}
that.setContent(that.content);
var $targetContent = that.getContentElement();
$targetContent.removeAttr('style');
that.displayContent();
if (that.options.async.success) {
that.options.async.success(that, data);
}
},
complete: function() {
that.xhr = null;
}
});
},
bindBodyEvents: function() {
if (this.options.dismissible && this.getTrigger() === 'click' && !_isBodyEventHandled) {
$document.off('keyup.webui-popover').on('keyup.webui-popover', $.proxy(this.escapeHandler, this));
$document.off('click.webui-popover touchend.webui-popover').on('click.webui-popover touchend.webui-popover', $.proxy(this.bodyClickHandler, this));
}
},
/* event handlers */
mouseenterHandler: function() {
var self = this;
if (self._timeout) {
clearTimeout(self._timeout);
}
self._enterTimeout = setTimeout(function() {
if (!self.getTarget().is(':visible')) {
self.show();
}
}, this.getDelayShow());
},
mouseleaveHandler: function() {
var self = this;
clearTimeout(self._enterTimeout);
//key point, set the _timeout then use clearTimeout when mouse leave
self._timeout = setTimeout(function() {
self.hide();
}, this.getHideDelay());
},
escapeHandler: function(e) {
if (e.keyCode === 27) {
this.hideAll();
}
},
bodyClickHandler: function() {
_isBodyEventHandled = true;
if (this.getTrigger() === 'click') {
if (this._targetclick) {
this._targetclick = false;
} else {
this.hideAll();
}
}
},
targetClickHandler: function() {
this._targetclick = true;
},
//reset and init the target events;
initTargetEvents: function() {
if (this.getTrigger() === 'hover') {
this.$target
.off('mouseenter mouseleave')
.on('mouseenter', $.proxy(this.mouseenterHandler, this))
.on('mouseleave', $.proxy(this.mouseleaveHandler, this));
}
this.$target.find('.close').off('click').on('click', $.proxy(this.hide, this, true));
this.$target.off('click.webui-popover').on('click.webui-popover', $.proxy(this.targetClickHandler, this));
},
/* utils methods */
//caculate placement of the popover
getPlacement: function(pos) {
var
placement,
de = document.documentElement,
db = document.body,
clientWidth = de.clientWidth,
clientHeight = de.clientHeight,
scrollTop = Math.max(db.scrollTop, de.scrollTop),
scrollLeft = Math.max(db.scrollLeft, de.scrollLeft),
pageX = Math.max(0, pos.left - scrollLeft),
pageY = Math.max(0, pos.top - scrollTop);
//arrowSize = 20;
//if placement equals autoļ¼caculate the placement by element information;
if (typeof(this.options.placement) === 'function') {
placement = this.options.placement.call(this, this.getTarget()[0], this.$element[0]);
} else {
placement = this.$element.data('placement') || this.options.placement;
}
var isH = placement === 'horizontal';
var isV = placement === 'vertical';
var detect = placement === 'auto' || isH || isV;
if (detect) {
if (pageX < clientWidth / 3) {
if (pageY < clientHeight / 3) {
placement = isH ? 'right-bottom' : 'bottom-right';
} else if (pageY < clientHeight * 2 / 3) {
if (isV) {
placement = pageY <= clientHeight / 2 ? 'bottom-right' : 'top-right';
} else {
placement = 'right';
}
} else {
placement = isH ? 'right-top' : 'top-right';
}
//placement= pageY>targetHeight+arrowSize?'top-right':'bottom-right';
} else if (pageX < clientWidth * 2 / 3) {
if (pageY < clientHeight / 3) {
if (isH) {
placement = pageX <= clientWidth / 2 ? 'right-bottom' : 'left-bottom';
} else {
placement = 'bottom';
}
} else if (pageY < clientHeight * 2 / 3) {
if (isH) {
placement = pageX <= clientWidth / 2 ? 'right' : 'left';
} else {
placement = pageY <= clientHeight / 2 ? 'bottom' : 'top';
}
} else {
if (isH) {
placement = pageX <= clientWidth / 2 ? 'right-top' : 'left-top';
} else {
placement = 'top';
}
}
} else {
//placement = pageY>targetHeight+arrowSize?'top-left':'bottom-left';
if (pageY < clientHeight / 3) {
placement = isH ? 'left-bottom' : 'bottom-left';
} else if (pageY < clientHeight * 2 / 3) {
if (isV) {
placement = pageY <= clientHeight / 2 ? 'bottom-left' : 'top-left';
} else {
placement = 'left';
}
} else {
placement = isH ? 'left-top' : 'top-left';
}
}
} else if (placement === 'auto-top') {
if (pageX < clientWidth / 3) {
placement = 'top-right';
} else if (pageX < clientWidth * 2 / 3) {
placement = 'top';
} else {
placement = 'top-left';
}
} else if (placement === 'auto-bottom') {
if (pageX < clientWidth / 3) {
placement = 'bottom-right';
} else if (pageX < clientWidth * 2 / 3) {
placement = 'bottom';
} else {
placement = 'bottom-left';
}
} else if (placement === 'auto-left') {
if (pageY < clientHeight / 3) {
placement = 'left-top';
} else if (pageY < clientHeight * 2 / 3) {
placement = 'left';
} else {
placement = 'left-bottom';
}
} else if (placement === 'auto-right') {
if (pageY < clientHeight / 3) {
placement = 'right-top';
} else if (pageY < clientHeight * 2 / 3) {
placement = 'right';
} else {
placement = 'right-bottom';
}
}
return placement;
},
getElementPosition: function() {
return $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth,
height: this.$element[0].offsetHeight
});
},
getTargetPositin: function(elementPos, placement, targetWidth, targetHeight) {
var pos = elementPos,
de = document.documentElement,
db = document.body,
clientWidth = de.clientWidth,
clientHeight = de.clientHeight,
elementW = this.$element.outerWidth(),
elementH = this.$element.outerHeight(),
scrollTop = Math.max(db.scrollTop, de.scrollTop),
scrollLeft = Math.max(db.scrollLeft, de.scrollLeft),
position = {},
arrowOffset = null,
arrowSize = this.options.arrow ? 20 : 0,
padding = 10,
fixedW = elementW < arrowSize + padding ? arrowSize : 0,
fixedH = elementH < arrowSize + padding ? arrowSize : 0,
refix = 0,
pageH = clientHeight + scrollTop,
pageW = clientWidth + scrollLeft;
var validLeft = pos.left + pos.width / 2 - fixedW > 0;
var validRight = pos.left + pos.width / 2 + fixedW < pageW;
var validTop = pos.top + pos.height / 2 - fixedH > 0;
var validBottom = pos.top + pos.height / 2 + fixedH < pageH;
switch (placement) {
case 'bottom':
position = {
top: pos.top + pos.height,
left: pos.left + pos.width / 2 - targetWidth / 2
};
break;
case 'top':
position = {
top: pos.top - targetHeight,
left: pos.left + pos.width / 2 - targetWidth / 2
};
break;
case 'left':
position = {
top: pos.top + pos.height / 2 - targetHeight / 2,
left: pos.left - targetWidth
};
break;
case 'right':
position = {
top: pos.top + pos.height / 2 - targetHeight / 2,
left: pos.left + pos.width
};
break;
case 'top-right':
position = {
top: pos.top - targetHeight,
left: validLeft ? pos.left - fixedW : padding
};
arrowOffset = {
left: validLeft ? Math.min(elementW, targetWidth) / 2 + fixedW : _offsetOut
};
break;
case 'top-left':
refix = validRight ? fixedW : -padding;
position = {
top: pos.top - targetHeight,
left: pos.left - targetWidth + pos.width + refix
};
arrowOffset = {
left: validRight ? targetWidth - Math.min(elementW, targetWidth) / 2 - fixedW : _offsetOut
};
break;
case 'bottom-right':
position = {
top: pos.top + pos.height,
left: validLeft ? pos.left - fixedW : padding
};
arrowOffset = {
left: validLeft ? Math.min(elementW, targetWidth) / 2 + fixedW : _offsetOut
};
break;
case 'bottom-left':
refix = validRight ? fixedW : -padding;
position = {
top: pos.top + pos.height,
left: pos.left - targetWidth + pos.width + refix
};
arrowOffset = {
left: validRight ? targetWidth - Math.min(elementW, targetWidth) / 2 - fixedW : _offsetOut
};
break;
case 'right-top':
refix = validBottom ? fixedH : -padding;
position = {
top: pos.top - targetHeight + pos.height + refix,
left: pos.left + pos.width
};
arrowOffset = {
top: validBottom ? targetHeight - Math.min(elementH, targetHeight) / 2 - fixedH : _offsetOut
};
break;
case 'right-bottom':
position = {
top: validTop ? pos.top - fixedH : padding,
left: pos.left + pos.width
};
arrowOffset = {
top: validTop ? Math.min(elementH, targetHeight) / 2 + fixedH : _offsetOut
};
break;
case 'left-top':
refix = validBottom ? fixedH : -padding;
position = {
top: pos.top - targetHeight + pos.height + refix,
left: pos.left - targetWidth
};
arrowOffset = {
top: validBottom ? targetHeight - Math.min(elementH, targetHeight) / 2 - fixedH : _offsetOut
};
break;
case 'left-bottom':
position = {
top: validTop ? pos.top - fixedH : padding,
left: pos.left - targetWidth
};
arrowOffset = {
top: validTop ? Math.min(elementH, targetHeight) / 2 + fixedH : _offsetOut
};
break;
}
return {
position: position,
arrowOffset: arrowOffset
};
}
};
$.fn[pluginName] = function(options, noInit) {
var results = [];
var $result = this.each(function() {
var webuiPopover = $.data(this, 'plugin_' + pluginName);
if (!webuiPopover) {
if (!options) {
webuiPopover = new WebuiPopover(this, null);
} else if (typeof options === 'string') {
if (options !== 'destroy') {
if (!noInit) {
webuiPopover = new WebuiPopover(this, null);
results.push(webuiPopover[options]());
}
}
} else if (typeof options === 'object') {
webuiPopover = new WebuiPopover(this, options);
}
$.data(this, 'plugin_' + pluginName, webuiPopover);
} else {
if (options === 'destroy') {
webuiPopover.destroy();
} else if (typeof options === 'string') {
results.push(webuiPopover[options]());
}
}
});
return (results.length) ? results : $result;
};
})(jQuery, window, document);