Merge branch 'hotfix/21.56.9' into master
[gitter.git] / public / js / views / popover.js
blob8885dc246926049f0f2e14dad40f9e33f5290e73
1 /* eslint complexity: ["error", 13] */
2 'use strict';
3 var $ = require('jquery');
4 var _ = require('lodash');
5 var Marionette = require('backbone.marionette');
6 let Mutant;
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;
16 var DEFAULTS = {
17   animation: true,
18   selector: false,
19   title: '',
20   footerView: null,
21   delay: 300,
22   container: false,
23   placement: 'right',
24   scroller: null,
25   width: '',
26   minHeight: ''
29 function findMaxZIndex(element) {
30   var max = 0;
31   while (element && element != document) {
32     var style = window.getComputedStyle(element, null);
34     if (style) {
35       var zIndex = style.getPropertyValue('z-index');
36       if (zIndex && zIndex !== 'auto') {
37         zIndex = parseInt(zIndex, 10);
38         if (zIndex > max) {
39           max = zIndex;
40         }
41       }
42     }
44     element = element.parentNode;
45   }
47   return max;
50 function generatePopoverPosition(placement, targetPos, actualWidth, actualHeight) {
51   var tp;
52   switch (placement) {
53     case 'bottom':
54       tp = {
55         top: targetPos.top + targetPos.height,
56         left: targetPos.left + targetPos.width / 2 - actualWidth / 2
57       };
58       break;
59     case 'top':
60       tp = {
61         top: targetPos.top - actualHeight,
62         left: targetPos.left + targetPos.width / 2 - actualWidth / 2 - 2
63       };
64       break;
65     case 'left':
66       tp = {
67         top: targetPos.top + targetPos.height / 2 - actualHeight / 2,
68         right: document.body.clientWidth - targetPos.left + ARROW_WIDTH_PX
69       };
70       break;
71     case 'right':
72       tp = {
73         top: targetPos.top + targetPos.height / 2 - actualHeight / 2,
74         left: targetPos.left + targetPos.width
75       };
76       break;
77   }
79   return tp;
82 var Popover = Marionette.ItemView.extend({
83   template: popoverTemplate,
84   className: 'popover',
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];
97     }
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();
108     });
109   },
111   serializeData: function() {
112     return this.options;
113   },
115   onRender: function() {
116     var $e = this.$el;
118     this.view.parentPopover = this;
120     if (this.zIndex) {
121       this.el.style.zIndex = this.zIndex + 1;
122     }
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);
128     } else {
129       $e.find('.popover-title').hide();
130     }
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;
139     if (fv) {
140       fv.parentPopover = this;
141       $e.find('.popover-footer-content').append(fv.render().el);
142     }
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');
149   },
151   enter: function() {
152     if (this.timeout) clearTimeout(this.timeout);
153   },
155   leave: function() {
156     if (!this.options.delay) {
157       return self.hide();
158     }
160     var self = this;
161     this.timeout = setTimeout(function() {
162       self.hide();
163     }, self.options.delay);
164   },
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();
175     }
176     this.view.destroy();
177     if (this.footerView) {
178       this.footerView.destroy();
179     }
180   },
182   show: function() {
183     var $e = this.render().$el;
184     var e = this.el;
186     $e.detach().css({ left: 'auto', display: 'block' });
187     $e.appendTo($('body'));
188     this.reposition();
190     $e.removeClass('popover-hidden');
192     if (Mutant && Mutant.prototype && Mutant.prototype.constructor) {
193       this.mutant = new Mutant(e, this.reposition, {
194         scope: this,
195         timeout: 20,
196         transitions: true,
197         observers: {
198           attributes: true,
199           characterData: true
200         }
201       });
202     }
203   },
205   reposition: function() {
206     try {
207       var $e = this.$el;
208       var e = this.el;
209       var pos = this.getTargetPosition();
210       if (pos.top === 0 && pos.left === 0 && pos.height === 0 && pos.width === 0) {
211         /* Do not reposition */
212         return;
213       }
215       var actualWidth = e.offsetWidth;
216       var actualHeight = e.offsetHeight;
218       var placement = this.options.placement;
219       switch (placement) {
220         case 'vertical':
221           placement = this.selectBestVerticalPlacement($e, this.targetElement);
222           break;
223         case 'horizontal':
224           placement = this.selectBestHorizontalPlacement($e, this.targetElement);
225           break;
226       }
228       var tp = generatePopoverPosition(placement, pos, actualWidth, actualHeight);
230       this.applyPlacement(tp, placement);
231     } finally {
232       // This is very important. If you leave it out, Chrome will likely crash.
233       if (this.mutant) this.mutant.takeRecords();
234     }
235   },
237   selectBestVerticalPlacement: function(div, target) {
238     var $target = $(target);
240     if (this.scroller) {
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) {
245         return 'top';
246       } else {
247         return 'bottom';
248       }
249     }
251     var panel = $target.offsetParent();
252     if (!panel) return 'bottom';
253     if ($target.offset().top + div.height() + 20 >= panel[0].clientHeight) {
254       return 'top';
255     }
257     return 'bottom';
258   },
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) {
266       return 'right';
267     } else {
268       return 'left';
269     }
270   },
272   // eslint-disable-next-line complexity, max-statements
273   applyPlacement: function(offset, placement) {
274     var $e = this.$el;
275     var e = $e[0];
277     var width = e.offsetWidth;
278     var height = e.offsetHeight;
279     var actualWidth = e.offsetWidth;
280     var actualHeight = e.offsetHeight;
281     var arrowDelta = 0;
282     var replace;
284     /* Adjust */
285     if (placement == 'bottom' || placement == 'top') {
286       if (offset.left < 0) {
287         arrowDelta = -2 * offset.left;
288         offset.left = 0;
289       } else if (offset.left + actualWidth > window.innerWidth) {
290         var overflow = window.innerWidth - (offset.left + actualWidth);
291         arrowDelta = 2 * overflow;
292         offset.left += overflow;
293       }
294     } else {
295       if (offset.top < 0) {
296         arrowDelta = -2 * offset.top;
297         offset.top = 0;
298       } else {
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;
303         }
304       }
305     }
307     var newPosition = {
308       top: offset.top
309     };
311     if (offset.right) {
312       newPosition.left = 'initial';
313       newPosition.right = offset.right;
314     } else {
315       newPosition.left = offset.left;
316       newPosition.right = 'initial';
317     }
319     $e.css(newPosition)
320       .addClass(placement)
321       .addClass('in');
323     if (placement == 'top' && actualHeight != height) {
324       offset.top = offset.top + height - actualHeight;
325       replace = true;
326     }
328     if (placement == 'bottom' || placement == 'top') {
329       this.replaceArrow(arrowDelta - width + actualWidth, actualWidth, 'left');
330     } else {
331       this.replaceArrow(arrowDelta - height + actualHeight, actualHeight, 'top');
332     }
333     if (replace) $e.offset(offset);
334   },
336   replaceArrow: function(delta, dimension, position) {
337     this.arrow().css(position, delta ? 50 * (1 - delta / dimension) + '%' : '');
338   },
340   hide: function() {
341     if (this.timeout) clearTimeout(this.timeout);
343     var $e = this.$el;
345     $e.removeClass('in');
347     $e.addClass('popover-hidden');
348     setTimeout(function() {
349       $e.detach();
350     }, 350);
352     $e.trigger('hidden');
353     this.trigger('hide');
354     this.destroy();
356     return this;
357   },
359   getTargetPosition: function() {
360     var el = this.targetElement;
362     const pos = {};
363     _.forIn(el.getBoundingClientRect(), (v, k) => (pos[k] = v));
364     _.forIn(this.$targetElement.offset(), (v, k) => (pos[k] = v));
366     return pos;
367   },
369   getTitle: function() {
370     return this.options.title;
371   },
373   arrow: function() {
374     if (!this.$arrow) {
375       this.$arrow = this.$el.find('.arrow');
376     }
378     return this.$arrow;
379   }
382 Popover.hoverTimeout = function(e, callback, scope) {
383   var timeout = setTimeout(function() {
384     if (!timeout) return;
385     callback.call(scope, e);
386   }, HOVER_DELAY);
388   $(e.target).one('mouseout click', function() {
389     clearTimeout(timeout);
390     timeout = null;
391   });
394 Popover.singleton = function(view, popover) {
395   view.popover = popover;
396   view.listenToOnce(popover, 'hide', function() {
397     view.popover = null;
398   });
401 module.exports = Popover;