Merge branch 'hotfix/21.56.9' into master
[gitter.git] / public / js / views / controls / dropdown.js
blob2355b237bce9a2d37c72565b7826a4d3ae433af3
1 'use strict';
2 var $ = require('jquery');
3 var _ = require('lodash');
4 var Marionette = require('backbone.marionette');
5 var cocktail = require('backbone.cocktail');
6 var Mutant = require('mutantjs');
7 var SelectableMixin = require('./selectable-mixin');
8 var itemTemplate = require('./tmpl/dropdownItem.hbs');
9 var dataset = require('../../utils/dataset-shim');
11 module.exports = (function() {
12   /* Transition period on css */
13   var TRANSITION = 160;
15   function findMaxZIndex(element) {
16     var max = 0;
17     while (element && element != document) {
18       var style = window.getComputedStyle(element, null);
20       if (style) {
21         var zIndex = style.getPropertyValue('z-index');
22         if (zIndex && zIndex !== 'auto') {
23           zIndex = parseInt(zIndex, 10);
24           if (zIndex > max) {
25             max = zIndex;
26           }
27         }
28       }
30       element = element.parentNode;
31     }
33     return max;
34   }
36   var backdropClass = 'dropdown-backdrop';
37   var backdropSelector = '.' + backdropClass;
39   var DropdownItemView = Marionette.ItemView.extend({
40     tagName: 'li',
41     template: itemTemplate,
42     initialize: function(options) {
43       if (options && options.template) {
44         this.template = options.template;
45       }
46       if (options && options.serializeData) {
47         this.serializeData = options.serializeData;
48       }
49     },
50     className: function() {
51       if (this.model.get('divider')) {
52         return 'divider';
53       }
55       return '';
56     },
57     onRender: function() {
58       /** Add anything in the dataset attribute to the Anchor tag's dataset */
59       var ds = this.model.get('dataset');
60       if (ds) {
61         var a = this.el.querySelector('a');
63         if (a) {
64           Object.keys(ds).forEach(function(key) {
65             dataset.set(a, key, ds[key]);
66           });
67         }
68       }
70       dataset.set(this.el, 'cid', this.model.cid);
71     }
72   });
74   var activeDropdown = null;
76   var DEFAULTS = {
77     placement: 'left'
78   };
80   var DropdownMenuView = Marionette.CollectionView.extend({
81     childView: DropdownItemView,
82     tagName: 'ul',
83     className: 'dropdown dropdown-hidden selectable',
84     ui: {
85       menu: 'ul.dropdown'
86     },
87     events: {
88       keydown: 'keydown',
89       'click li a': 'clicked'
90     },
92     childViewOptions: function() {
93       var options = {};
94       if (this.options.itemTemplate) {
95         options.template = this.options.itemTemplate;
96       }
97       if (this.options.itemSerializeData) {
98         options.serializeData = this.options.itemSerializeData;
99       }
100       return options;
101     },
103     initialize: function(options) {
104       if (options.targetElement) {
105         this.setTargetElement(options.targetElement);
106       }
108       this.options = _.extend({}, DEFAULTS, options);
110       this.dropdownClass = options.dropdownClass;
112       /* From the selectable-mixin */
113       this.listenTo(this, 'selectClicked', function() {
114         this.hide();
115       });
116     },
118     setTargetElement: function(el) {
119       this.targetElement = el;
120       this.$targetElement = $(el);
121     },
123     active: function() {
124       return !this.$el.hasClass('dropdown-hidden');
125     },
127     onRender: function() {
128       var zIndex = findMaxZIndex(this.targetElement) + 5;
129       if (zIndex < 100) {
130         zIndex = 100;
131       }
132       this.el.style.zIndex = zIndex;
133       if (this.dropdownClass) {
134         this.el.classList.add(this.dropdownClass);
135       }
136     },
138     onDestroy: function() {
139       if (this.mutant) this.mutant.disconnect();
140       $(backdropSelector).off(this.backdropClickedCallback);
141     },
143     clicked: function() {
144       if (!this.collection) {
145         /* Static */
146         this.hide();
147       }
148     },
149     backdropClickedCallback: function() {
150       $(backdropSelector).remove();
151       if (activeDropdown) {
152         var t = activeDropdown;
153         activeDropdown = null;
154         t.hide();
155       }
156     },
158     getPosition: function() {
159       var el = this.targetElement;
161       const pos = {};
162       _.forIn(el.getBoundingClientRect(), (v, k) => (pos[k] = v));
163       _.forIn(this.$targetElement.offset(), (v, k) => (pos[k] = v));
165       return pos;
166     },
168     hasItems: function() {
169       if (this.collection) return this.collection.length > 0;
171       // Static mode
172       return true;
173     },
175     onAddChild: function() {
176       setTimeout(
177         function() {
178           if (!this.active() && this.showWhenItems && this.hasItems()) {
179             this.show();
180           }
181         }.bind(this),
182         10
183       );
184     },
186     onRemoveChild: function() {
187       setTimeout(
188         function() {
189           if (!this.hasItems()) {
190             this.hide();
191             this.showWhenItems = true;
192           }
193         }.bind(this),
194         10
195       );
196     },
198     show: function() {
199       if (this.active()) return;
201       if (!this.hasItems()) {
202         this.showWhenItems = true;
203         return;
204       }
206       var $e = this.render().$el;
207       var e = this.el;
209       // Stop any impending actions from hide
210       window.clearTimeout(this.hideTimeoutId);
212       $(backdropSelector).remove();
213       if (activeDropdown) {
214         activeDropdown.hide();
215       }
217       activeDropdown = this;
219       var zIndex = parseInt(this.el.style.zIndex, 10);
220       // Create the backdrop
221       document
222         .querySelector('body')
223         .insertAdjacentHTML(
224           'beforeend',
225           `<div class="${backdropClass} ${this.options.backdropClass}" style="z-index: ${zIndex -
226             1}" />`
227         );
229       // Add the click event listener
230       $(backdropSelector).on('click', this.backdropClickedCallback);
232       this.setActive(this.selectedModel);
234       $e.detach().css({ top: 0, left: 0, display: 'block' });
235       $e.appendTo($('body'));
236       this.reposition();
238       $e.removeClass('dropdown-hidden');
240       if (!this.mutant) {
241         this.mutant = new Mutant(e, this.mutationReposition, {
242           scope: this,
243           timeout: 20,
244           transitions: true,
245           observers: {
246             attributes: true,
247             characterData: true
248           }
249         });
250       }
251     },
253     hide: function() {
254       var $el = this.$el;
255       this.showWhenItems = false;
256       if (!this.active()) return;
257       // $el.find('li.active:not(.divider):visible').removeClass('active');
258       $el.addClass('dropdown-hidden');
259       $(backdropSelector).remove();
261       this.hideTimeoutId = window.setTimeout(function() {
262         $el.css({ display: 'none' });
263       }, TRANSITION);
264       activeDropdown = null;
265     },
267     mutationReposition: function() {
268       try {
269         if (!this.active()) return;
270         this.reposition();
271       } finally {
272         // This is very important. If you leave it out, Chrome will likely crash.
273         if (this.mutant) this.mutant.takeRecords();
274       }
275     },
277     reposition: function() {
278       var e = this.el;
279       var pos = this.getPosition();
281       var actualWidth = e.offsetWidth;
283       var left;
284       if (this.options.placement === 'left') {
285         left = pos.left;
286       } else {
287         left = pos.left - actualWidth + pos.width;
288       }
290       var tp = { top: pos.top + pos.height, left: left };
291       this.applyPlacement(tp);
292     },
294     applyPlacement: function(offset) {
295       var $e = this.$el;
297       var replace;
299       /* Adjust */
300       if ('left' in offset && offset.left < 0) {
301         offset.left = 0;
302       }
304       $e.css(offset);
306       if (replace) $e.offset(offset);
307     },
309     toggle: function() {
310       var isActive = this.active();
311       if (isActive) {
312         this.hide();
313       } else {
314         this.show();
315       }
316     },
318     keydown: function(e) {
319       switch (e.keyCode) {
320         case 13:
321           this.selected();
322           return;
323         case 27:
324           this.hide();
325           break;
326         default:
327           return;
328       }
330       e.preventDefault();
331       e.stopPropagation();
332     }
333   });
334   cocktail.mixin(DropdownMenuView, SelectableMixin);
335   return DropdownMenuView;
336 })();