1 /* eslint complexity: ["error", 13] */
3 var $ = require('jquery');
4 var _ = require('lodash');
5 var Marionette = require('backbone.marionette');
7 if (typeof window !== 'undefined') {
8 Mutant = require('mutantjs');
10 var popoverTemplate = require('./tmpl/popover.hbs');
12 var ARROW_WIDTH_PX = 10;
14 var HOVER_DELAY = 750;
29 function findMaxZIndex(element) {
31 while (element && element != document) {
32 var style = window.getComputedStyle(element, null);
35 var zIndex = style.getPropertyValue('z-index');
36 if (zIndex && zIndex !== 'auto') {
37 zIndex = parseInt(zIndex, 10);
44 element = element.parentNode;
50 function generatePopoverPosition(placement, targetPos, actualWidth, actualHeight) {
55 top: targetPos.top + targetPos.height,
56 left: targetPos.left + targetPos.width / 2 - actualWidth / 2
61 top: targetPos.top - actualHeight,
62 left: targetPos.left + targetPos.width / 2 - actualWidth / 2 - 2
67 top: targetPos.top + targetPos.height / 2 - actualHeight / 2,
68 right: document.body.clientWidth - targetPos.left + ARROW_WIDTH_PX
73 top: targetPos.top + targetPos.height / 2 - actualHeight / 2,
74 left: targetPos.left + targetPos.width
82 var Popover = Marionette.ItemView.extend({
83 template: popoverTemplate,
85 initialize: function(options) {
86 _.bindAll(this, 'leave', 'enter');
87 this.options = _.extend({}, DEFAULTS, options);
88 //this.init('popover', element, options);
89 this.view = this.options.view;
90 this.titleView = this.options.titleView;
91 this.title = this.options.title;
92 this.footerView = this.options.footerView;
94 if (this.options.scroller) {
95 this.$scroller = $(this.options.scroller);
96 this.scroller = this.$scroller[0];
99 this.targetElement = this.options.targetElement;
100 this.$targetElement = $(this.targetElement);
102 this.zIndex = findMaxZIndex(this.targetElement);
104 this.$targetElement.on('mouseenter', this.enter);
105 this.$targetElement.on('mouseleave', this.leave);
106 this.once('destroy', function() {
107 if (this.mutant) this.mutant.disconnect();
111 serializeData: function() {
115 onRender: function() {
118 this.view.parentPopover = this;
121 this.el.style.zIndex = this.zIndex + 1;
124 if (this.titleView) {
125 $e.find('.popover-title').append(this.titleView.render().el);
126 } else if (this.title) {
127 $e.find('.popover-title').text(this.title);
129 $e.find('.popover-title').hide();
132 $e.find('.popover-content').append(this.view.render().el);
133 $e.find('.popover-inner')
134 .css('width', this.options.width)
135 .css('min-height', this.options.minHeight);
137 var fv = this.footerView;
140 fv.parentPopover = this;
141 $e.find('.popover-footer-content').append(fv.render().el);
144 $e.on('mouseenter', this.enter);
145 $e.on('mouseleave', this.leave);
147 $e.addClass('popover-hidden');
148 $e.removeClass('fade top bottom left right in');
152 if (this.timeout) clearTimeout(this.timeout);
156 if (!this.options.delay) {
161 this.timeout = setTimeout(function() {
163 }, self.options.delay);
166 onDestroy: function() {
167 this.$el.off('mouseenter', this.enter);
168 this.$el.off('mouseleave', this.leave);
170 this.$targetElement.off('mouseenter', this.enter);
171 this.$targetElement.off('mouseleave', this.leave);
173 if (this.titleView) {
174 this.titleView.destroy();
177 if (this.footerView) {
178 this.footerView.destroy();
183 var $e = this.render().$el;
186 $e.detach().css({ left: 'auto', display: 'block' });
187 $e.appendTo($('body'));
190 $e.removeClass('popover-hidden');
192 if (Mutant && Mutant.prototype && Mutant.prototype.constructor) {
193 this.mutant = new Mutant(e, this.reposition, {
205 reposition: function() {
209 var pos = this.getTargetPosition();
210 if (pos.top === 0 && pos.left === 0 && pos.height === 0 && pos.width === 0) {
211 /* Do not reposition */
215 var actualWidth = e.offsetWidth;
216 var actualHeight = e.offsetHeight;
218 var placement = this.options.placement;
221 placement = this.selectBestVerticalPlacement($e, this.targetElement);
224 placement = this.selectBestHorizontalPlacement($e, this.targetElement);
228 var tp = generatePopoverPosition(placement, pos, actualWidth, actualHeight);
230 this.applyPlacement(tp, placement);
232 // This is very important. If you leave it out, Chrome will likely crash.
233 if (this.mutant) this.mutant.takeRecords();
237 selectBestVerticalPlacement: function(div, target) {
238 var $target = $(target);
241 var scrollTop = this.scroller.scrollTop;
242 var scrollBottom = this.scroller.scrollTop + this.scroller.clientHeight;
243 var middle = (scrollTop + scrollBottom) / 2;
244 if (target.offsetTop > middle) {
251 var panel = $target.offsetParent();
252 if (!panel) return 'bottom';
253 if ($target.offset().top + div.height() + 20 >= panel[0].clientHeight) {
260 selectBestHorizontalPlacement: function(div, target) {
261 // jshint unused:true
262 // var $target = $(target);
264 var bounds = target.getBoundingClientRect();
265 if (bounds.left < document.body.clientWidth / 2) {
272 // eslint-disable-next-line complexity, max-statements
273 applyPlacement: function(offset, placement) {
277 var width = e.offsetWidth;
278 var height = e.offsetHeight;
279 var actualWidth = e.offsetWidth;
280 var actualHeight = e.offsetHeight;
285 if (placement == 'bottom' || placement == 'top') {
286 if (offset.left < 0) {
287 arrowDelta = -2 * offset.left;
289 } else if (offset.left + actualWidth > window.innerWidth) {
290 var overflow = window.innerWidth - (offset.left + actualWidth);
291 arrowDelta = 2 * overflow;
292 offset.left += overflow;
295 if (offset.top < 0) {
296 arrowDelta = -2 * offset.top;
299 var clientHeight = this.scroller ? this.scroller.clientHeight : window.innerHeight;
300 if (offset.top + height > clientHeight) {
301 arrowDelta = 2 * (clientHeight - offset.top - height - 10);
302 offset.top = clientHeight - height - 10;
312 newPosition.left = 'initial';
313 newPosition.right = offset.right;
315 newPosition.left = offset.left;
316 newPosition.right = 'initial';
323 if (placement == 'top' && actualHeight != height) {
324 offset.top = offset.top + height - actualHeight;
328 if (placement == 'bottom' || placement == 'top') {
329 this.replaceArrow(arrowDelta - width + actualWidth, actualWidth, 'left');
331 this.replaceArrow(arrowDelta - height + actualHeight, actualHeight, 'top');
333 if (replace) $e.offset(offset);
336 replaceArrow: function(delta, dimension, position) {
337 this.arrow().css(position, delta ? 50 * (1 - delta / dimension) + '%' : '');
341 if (this.timeout) clearTimeout(this.timeout);
345 $e.removeClass('in');
347 $e.addClass('popover-hidden');
348 setTimeout(function() {
352 $e.trigger('hidden');
353 this.trigger('hide');
359 getTargetPosition: function() {
360 var el = this.targetElement;
363 _.forIn(el.getBoundingClientRect(), (v, k) => (pos[k] = v));
364 _.forIn(this.$targetElement.offset(), (v, k) => (pos[k] = v));
369 getTitle: function() {
370 return this.options.title;
375 this.$arrow = this.$el.find('.arrow');
382 Popover.hoverTimeout = function(e, callback, scope) {
383 var timeout = setTimeout(function() {
384 if (!timeout) return;
385 callback.call(scope, e);
388 $(e.target).one('mouseout click', function() {
389 clearTimeout(timeout);
394 Popover.singleton = function(view, popover) {
395 view.popover = popover;
396 view.listenToOnce(popover, 'hide', function() {
401 module.exports = Popover;