blob: 57814dff2ba642675969270fc7382bd07479c578 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window, angular, undefined){
'use strict';
var module = angular.module('nsPopover', []);
var $el = angular.element;
var isDef = angular.isDefined;
var $popovers = [];
var globalId = 0;
module.provider('nsPopover', function () {
var defaults = {
template: '',
theme: 'ns-popover-list-theme',
plain: 'false',
trigger: 'click',
triggerPrevent: true,
angularEvent: '',
scopeEvent: '',
container: 'body',
placement: 'bottom|left',
timeout: 1.5,
hideOnInsideClick: false,
hideOnOutsideClick: true,
hideOnButtonClick: true,
mouseRelative: '',
popupDelay: 0
};
this.setDefaults = function(newDefaults) {
angular.extend(defaults, newDefaults);
};
this.$get = function () {
return {
getDefaults: function () {
return defaults;
}
};
};
});
module.directive('nsPopover', ['nsPopover','$rootScope','$timeout','$templateCache','$q','$http','$compile','$document','$parse',
function(nsPopover, $rootScope, $timeout, $templateCache, $q, $http, $compile, $document, $parse) {
return {
restrict: 'A',
scope: true,
link: function(scope, elm, attrs) {
var defaults = nsPopover.getDefaults();
var options = {
template: attrs.nsPopoverTemplate || defaults.template,
theme: attrs.nsPopoverTheme || defaults.theme,
plain: toBoolean(attrs.nsPopoverPlain || defaults.plain),
trigger: attrs.nsPopoverTrigger || defaults.trigger,
triggerPrevent: attrs.nsPopoverTriggerPrevent || defaults.triggerPrevent,
angularEvent: attrs.nsPopoverAngularEvent || defaults.angularEvent,
scopeEvent: attrs.nsPopoverScopeEvent || defaults.scopeEvent,
container: attrs.nsPopoverContainer || defaults.container,
placement: attrs.nsPopoverPlacement || defaults.placement,
timeout: attrs.nsPopoverTimeout || defaults.timeout,
hideOnInsideClick: toBoolean(attrs.nsPopoverHideOnInsideClick || defaults.hideOnInsideClick),
hideOnOutsideClick: toBoolean(attrs.nsPopoverHideOnOutsideClick || defaults.hideOnOutsideClick),
hideOnButtonClick: toBoolean(attrs.nsPopoverHideOnButtonClick || defaults.hideOnButtonClick),
mouseRelative: attrs.nsPopoverMouseRelative,
popupDelay: attrs.nsPopoverPopupDelay || defaults.popupDelay,
group: attrs.nsPopoverGroup
};
if (options.mouseRelative) {
options.mouseRelativeX = options.mouseRelative.indexOf('x') !== -1;
options.mouseRelativeY = options.mouseRelative.indexOf('y') !== -1;
}
var displayer_ = {
id_: undefined,
/**
* Set the display property of the popover to 'block' after |delay| milliseconds.
*
* @param delay {Number} The time (in seconds) to wait before set the display property.
* @param e {Event} The event which caused the popover to be shown.
*/
display: function(delay, e) {
// Disable popover if ns-popover value is false
if ($parse(attrs.nsPopover)(scope) === false) {
return;
}
$timeout.cancel(displayer_.id_);
if (!isDef(delay)) {
delay = 0;
}
// hide any popovers being displayed
if (options.group) {
$rootScope.$broadcast('ns:popover:hide', options.group);
}
displayer_.id_ = $timeout(function() {
$popover.isOpen = true;
$popover.css('display', 'block');
// position the popover accordingly to the defined placement around the
// |elm|.
var elmRect = getBoundingClientRect(elm[0]);
// If the mouse-relative options is specified we need to adjust the
// element client rect to the current mouse coordinates.
if (options.mouseRelative) {
elmRect = adjustRect(elmRect, options.mouseRelativeX, options.mouseRelativeY, e);
}
move($popover, placement_, align_, elmRect, $triangle);
if (options.hideOnInsideClick) {
// Hide the popover without delay on the popover click events.
$popover.on('click', insideClickHandler);
}
if (options.hideOnOutsideClick) {
// Hide the popover without delay on outside click events.
$document.on('click', outsideClickHandler);
}
if (options.hideOnButtonClick) {
// Hide the popover without delay on the button click events.
elm.on('click', buttonClickHandler);
}
}, delay*1000);
},
cancel: function() {
$timeout.cancel(displayer_.id_);
}
};
var hider_ = {
id_: undefined,
/**
* Set the display property of the popover to 'none' after |delay| milliseconds.
*
* @param delay {Number} The time (in seconds) to wait before set the display property.
*/
hide: function(delay) {
$timeout.cancel(hider_.id_);
// delay the hiding operation for 1.5s by default.
if (!isDef(delay)) {
delay = 1.5;
}
hider_.id_ = $timeout(function() {
$popover.off('click', insideClickHandler);
$document.off('click', outsideClickHandler);
elm.off('click', buttonClickHandler);
$popover.isOpen = false;
displayer_.cancel();
$popover.css('display', 'none');
}, delay*1000);
},
cancel: function() {
$timeout.cancel(hider_.id_);
}
};
var $container = $document.find(options.container);
if (!$container.length) {
$container = $document.find('body');
}
var $triangle;
var placement_;
var align_;
globalId += 1;
var $popover = $el('<div id="nspopover-' + globalId +'"></div>');
$popovers.push($popover);
var match = options.placement
.match(/^(top|bottom|left|right)$|((top|bottom)\|(center|left|right)+)|((left|right)\|(center|top|bottom)+)/);
if (!match) {
throw new Error('"' + options.placement + '" is not a valid placement or has a invalid combination of placements.');
}
placement_ = match[6] || match[3] || match[1];
align_ = match[7] || match[4] || match[2] || 'center';
$q.when(loadTemplate(options.template, options.plain)).then(function(template) {
template = angular.isString(template) ?
template :
template.data && angular.isString(template.data) ?
template.data :
'';
$popover.html(template);
if (options.theme) {
$popover.addClass(options.theme);
}
// Add classes that identifies the placement and alignment of the popver
// which allows the customization of the popover based on its position.
$popover
.addClass('ns-popover-' + placement_ + '-placement')
.addClass('ns-popover-' + align_ + '-align');
$compile($popover)(scope);
scope.$on('$destroy', function() {
$popover.remove();
});
scope.hidePopover = function() {
hider_.hide(0);
};
scope.$on('ns:popover:hide', function(ev, group) {
if (options.group === group) {
scope.hidePopover();
}
});
$popover
.css('position', 'absolute')
.css('display', 'none');
//search for the triangle element - works in ie8+
$triangle = $popover[0].querySelectorAll('.triangle');
//if the element is found, then convert it to an angular element
if($triangle.length){
$triangle = $el($triangle);
}
$container.append($popover);
});
if (options.angularEvent) {
$rootScope.$on(options.angularEvent, function() {
hider_.cancel();
displayer_.display(options.popupDelay);
});
} else if (options.scopeEvent) {
scope.$on(options.scopeEvent, function() {
hider_.cancel();
displayer_.display($popover, options.popupDelay);
});
} else {
elm.on(options.trigger, function(e) {
if (false !== options.triggerPrevent) {
e.preventDefault();
}
hider_.cancel();
displayer_.display(options.popupDelay, e);
});
}
elm
.on('mouseout', function() {
hider_.hide(options.timeout);
})
.on('mouseover', function() {
hider_.cancel();
});
$popover
.on('mouseout', function(e) {
hider_.hide(options.timeout);
})
.on('mouseover', function() {
hider_.cancel();
});
/**
* Move the popover to the |placement| position of the object located on the |rect|.
*
* @param popover {Object} The popover object to be moved.
* @param placement {String} The relative position to move the popover - top | bottom | left | right.
* @param align {String} The way the popover should be aligned - center | left | right.
* @param rect {ClientRect} The ClientRect of the object to move the popover around.
* @param triangle {Object} The element that contains the popover's triangle. This can be null.
*/
function move(popover, placement, align, rect, triangle) {
var popoverRect = getBoundingClientRect(popover[0]);
var top, left;
var positionX = function() {
if (align === 'center') {
return Math.round(rect.left + rect.width/2 - popoverRect.width/2);
} else if(align === 'right') {
return rect.right - popoverRect.width;
}
return rect.left;
};
var positionY = function() {
if (align === 'center') {
return Math.round(rect.top + rect.height/2 - popoverRect.height/2);
} else if(align === 'bottom') {
return rect.bottom - popoverRect.height;
}
return rect.top;
};
if (placement === 'top') {
top = rect.top - popoverRect.height;
left = positionX();
} else if (placement === 'right') {
top = positionY();
left = rect.right;
} else if (placement === 'bottom') {
top = rect.bottom;
left = positionX();
} else if (placement === 'left') {
top = positionY();
left = rect.left - popoverRect.width;
}
popover
.css('top', top.toString() + 'px')
.css('left', left.toString() + 'px');
if (triangle) {
if (placement === 'top' || placement === 'bottom') {
left = rect.left + rect.width / 2 - left;
triangle.css('left', left.toString() + 'px');
} else {
top = rect.top + rect.height / 2 - top;
triangle.css('top', top.toString() + 'px');
}
}
}
/**
* Adjust a rect accordingly to the given x and y mouse positions.
*
* @param rect {ClientRect} The rect to be adjusted.
*/
function adjustRect(rect, adjustX, adjustY, ev) {
// if pageX or pageY is defined we need to lock the popover to the given
// x and y position.
// clone the rect, so we can manipulate its properties.
var localRect = {
bottom: rect.bottom,
height: rect.height,
left: rect.left,
right: rect.right,
top: rect.top,
width: rect.width
};
if (adjustX) {
localRect.left = ev.pageX;
localRect.right = ev.pageX;
localRect.width = 0;
}
if (adjustY) {
localRect.top = ev.pageY;
localRect.bottom = ev.pageY;
localRect.height = 0;
}
return localRect;
}
function getBoundingClientRect(elm) {
var w = window;
var doc = document.documentElement || document.body.parentNode || document.body;
var x = (isDef(w.pageXOffset)) ? w.pageXOffset : doc.scrollLeft;
var y = (isDef(w.pageYOffset)) ? w.pageYOffset : doc.scrollTop;
var rect = elm.getBoundingClientRect();
// ClientRect class is immutable, so we need to return a modified copy
// of it when the window has been scrolled.
if (x || y) {
return {
bottom:rect.bottom+y,
left:rect.left + x,
right:rect.right + x,
top:rect.top + y,
height:rect.height,
width:rect.width
};
}
return rect;
}
function toBoolean(value) {
if (value && value.length !== 0) {
var v = ("" + value).toLowerCase();
value = (v == 'true');
} else {
value = false;
}
return value;
}
/**
* Load the given template in the cache if it is not already loaded.
*
* @param template The URI of the template to be loaded.
* @returns {String} A promise that the template will be loaded.
* @remarks If the template is null or undefined a empty string will be returned.
*/
function loadTemplate(template, plain) {
if (!template) {
return '';
}
if (angular.isString(template) && plain) {
return template;
}
return $templateCache.get(template) || $http.get(template, { cache : true });
}
function insideClickHandler() {
if ($popover.isOpen) {
hider_.hide(0);
}
}
function outsideClickHandler(e) {
if ($popover.isOpen && e.target !== elm[0]) {
var id = $popover[0].id;
if (!isInPopover(e.target)) {
hider_.hide(0);
}
}
function isInPopover(el) {
if (el.id === id) {
return true;
}
var parent = angular.element(el).parent()[0];
if (!parent) {
return false;
}
if (parent.id === id) {
return true;
}
else {
return isInPopover(parent);
}
}
}
function buttonClickHandler() {
if ($popover.isOpen) {
hider_.hide(0);
}
}
}
};
}
]);
})(window, window.angular);