ddbbe927e5771281885795933afd9a6c945e8c8c
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.sticky');
8 goog.require('ngmaterial.components.content');
9 goog.require('ngmaterial.core');
10 /**
11  * @ngdoc module
12  * @name material.components.sticky
13  * @description
14  * Sticky effects for md
15  *
16  */
17 MdSticky['$inject'] = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"];
18 angular
19   .module('material.components.sticky', [
20     'material.core',
21     'material.components.content'
22   ])
23   .factory('$mdSticky', MdSticky);
24
25 /**
26  * @ngdoc service
27  * @name $mdSticky
28  * @module material.components.sticky
29  *
30  * @description
31  * The `$mdSticky`service provides a mixin to make elements sticky.
32  *
33  * Whenever the current browser supports stickiness natively, the `$mdSticky` service will just
34  * use the native browser stickiness.
35  *
36  * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone`
37  * parameter, in the same scope as the actual element lives.
38  *
39  *
40  * <h3>Notes</h3>
41  * When using an element which is containing a compiled directive, which changed its DOM structure during compilation,
42  * you should compile the clone yourself using the plain template.<br/><br/>
43  * See the right usage below:
44  * <hljs lang="js">
45  *   angular.module('myModule')
46  *     .directive('stickySelect', function($mdSticky, $compile) {
47  *       var SELECT_TEMPLATE =
48  *         '<md-select ng-model="selected">' +
49  *           '<md-option>Option 1</md-option>' +
50  *         '</md-select>';
51  *
52  *       return {
53  *         restrict: 'E',
54  *         replace: true,
55  *         template: SELECT_TEMPLATE,
56  *         link: function(scope,element) {
57  *           $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope));
58  *         }
59  *       };
60  *     });
61  * </hljs>
62  *
63  * @usage
64  * <hljs lang="js">
65  *   angular.module('myModule')
66  *     .directive('stickyText', function($mdSticky, $compile) {
67  *       return {
68  *         restrict: 'E',
69  *         template: '<span>Sticky Text</span>',
70  *         link: function(scope,element) {
71  *           $mdSticky(scope, element);
72  *         }
73  *       };
74  *     });
75  * </hljs>
76  *
77  * @returns A `$mdSticky` function that takes three arguments:
78  *   - `scope`
79  *   - `element`: The element that will be 'sticky'
80  *   - `elementClone`: A clone of the element, that will be shown
81  *     when the user starts scrolling past the original element.
82  *     If not provided, it will use the result of `element.clone()` and compiles it in the given scope.
83  */
84 function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) {
85
86   var browserStickySupport = $mdUtil.checkStickySupport();
87
88   /**
89    * Registers an element as sticky, used internally by directives to register themselves
90    */
91   return function registerStickyElement(scope, element, stickyClone) {
92     var contentCtrl = element.controller('mdContent');
93     if (!contentCtrl) return;
94
95     if (browserStickySupport) {
96       element.css({
97         position: browserStickySupport,
98         top: 0,
99         'z-index': 2
100       });
101     } else {
102       var $$sticky = contentCtrl.$element.data('$$sticky');
103       if (!$$sticky) {
104         $$sticky = setupSticky(contentCtrl);
105         contentCtrl.$element.data('$$sticky', $$sticky);
106       }
107
108       // Compile our cloned element, when cloned in this service, into the given scope.
109       var cloneElement = stickyClone || $compile(element.clone())(scope);
110
111       var deregister = $$sticky.add(element, cloneElement);
112       scope.$on('$destroy', deregister);
113     }
114   };
115
116   function setupSticky(contentCtrl) {
117     var contentEl = contentCtrl.$element;
118
119     // Refresh elements is very expensive, so we use the debounced
120     // version when possible.
121     var debouncedRefreshElements = $$rAF.throttle(refreshElements);
122
123     // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
124     // more reliable than `scroll` on android.
125     setupAugmentedScrollEvents(contentEl);
126     contentEl.on('$scrollstart', debouncedRefreshElements);
127     contentEl.on('$scroll', onScroll);
128
129     var self;
130     return self = {
131       prev: null,
132       current: null, //the currently stickied item
133       next: null,
134       items: [],
135       add: add,
136       refreshElements: refreshElements
137     };
138
139     /***************
140      * Public
141      ***************/
142     // Add an element and its sticky clone to this content's sticky collection
143     function add(element, stickyClone) {
144       stickyClone.addClass('md-sticky-clone');
145
146       var item = {
147         element: element,
148         clone: stickyClone
149       };
150       self.items.push(item);
151
152       $mdUtil.nextTick(function() {
153         contentEl.prepend(item.clone);
154       });
155
156       debouncedRefreshElements();
157
158       return function remove() {
159         self.items.forEach(function(item, index) {
160           if (item.element[0] === element[0]) {
161             self.items.splice(index, 1);
162             item.clone.remove();
163           }
164         });
165         debouncedRefreshElements();
166       };
167     }
168
169     function refreshElements() {
170       // Sort our collection of elements by their current position in the DOM.
171       // We need to do this because our elements' order of being added may not
172       // be the same as their order of display.
173       self.items.forEach(refreshPosition);
174       self.items = self.items.sort(function(a, b) {
175         return a.top < b.top ? -1 : 1;
176       });
177
178       // Find which item in the list should be active, 
179       // based upon the content's current scroll position
180       var item;
181       var currentScrollTop = contentEl.prop('scrollTop');
182       for (var i = self.items.length - 1; i >= 0; i--) {
183         if (currentScrollTop > self.items[i].top) {
184           item = self.items[i];
185           break;
186         }
187       }
188       setCurrentItem(item);
189     }
190
191     /***************
192      * Private
193      ***************/
194
195     // Find the `top` of an item relative to the content element,
196     // and also the height.
197     function refreshPosition(item) {
198       // Find the top of an item by adding to the offsetHeight until we reach the 
199       // content element.
200       var current = item.element[0];
201       item.top = 0;
202       item.left = 0;
203       item.right = 0;
204       while (current && current !== contentEl[0]) {
205         item.top += current.offsetTop;
206         item.left += current.offsetLeft;
207         if ( current.offsetParent ){
208           item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight
209         }
210         current = current.offsetParent;
211       }
212       item.height = item.element.prop('offsetHeight');
213
214       var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined;
215       $mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal);
216       $mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right);
217     }
218
219     // As we scroll, push in and select the correct sticky element.
220     function onScroll() {
221       var scrollTop = contentEl.prop('scrollTop');
222       var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
223
224       // Store the previous scroll so we know which direction we are scrolling
225       onScroll.prevScrollTop = scrollTop;
226
227       //
228       // AT TOP (not scrolling)
229       //
230       if (scrollTop === 0) {
231         // If we're at the top, just clear the current item and return
232         setCurrentItem(null);
233         return;
234       }
235
236       //
237       // SCROLLING DOWN (going towards the next item)
238       //
239       if (isScrollingDown) {
240
241         // If we've scrolled down past the next item's position, sticky it and return
242         if (self.next && self.next.top <= scrollTop) {
243           setCurrentItem(self.next);
244           return;
245         }
246
247         // If the next item is close to the current one, push the current one up out of the way
248         if (self.current && self.next && self.next.top - scrollTop <= self.next.height) {
249           translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop));
250           return;
251         }
252       }
253
254       //
255       // SCROLLING UP (not at the top & not scrolling down; must be scrolling up)
256       //
257       if (!isScrollingDown) {
258
259         // If we've scrolled up past the previous item's position, sticky it and return
260         if (self.current && self.prev && scrollTop < self.current.top) {
261           setCurrentItem(self.prev);
262           return;
263         }
264
265         // If the next item is close to the current one, pull the current one down into view
266         if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) {
267           translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height));
268           return;
269         }
270       }
271
272       //
273       // Otherwise, just move the current item to the proper place (scrolling up or down)
274       //
275       if (self.current) {
276         translate(self.current, scrollTop);
277       }
278     }
279
280     function setCurrentItem(item) {
281       if (self.current === item) return;
282       // Deactivate currently active item
283       if (self.current) {
284         translate(self.current, null);
285         setStickyState(self.current, null);
286       }
287
288       // Activate new item if given
289       if (item) {
290         setStickyState(item, 'active');
291       }
292
293       self.current = item;
294       var index = self.items.indexOf(item);
295       // If index === -1, index + 1 = 0. It works out.
296       self.next = self.items[index + 1];
297       self.prev = self.items[index - 1];
298       setStickyState(self.next, 'next');
299       setStickyState(self.prev, 'prev');
300     }
301
302     function setStickyState(item, state) {
303       if (!item || item.state === state) return;
304       if (item.state) {
305         item.clone.attr('sticky-prev-state', item.state);
306         item.element.attr('sticky-prev-state', item.state);
307       }
308       item.clone.attr('sticky-state', state);
309       item.element.attr('sticky-state', state);
310       item.state = state;
311     }
312
313     function translate(item, amount) {
314       if (!item) return;
315       if (amount === null || amount === undefined) {
316         if (item.translateY) {
317           item.translateY = null;
318           item.clone.css($mdConstant.CSS.TRANSFORM, '');
319         }
320       } else {
321         item.translateY = amount;
322
323         $mdUtil.bidi( item.clone, $mdConstant.CSS.TRANSFORM,
324           'translate3d(' + item.left + 'px,' + amount + 'px,0)',
325           'translateY(' + amount + 'px)'
326         );
327       }
328     }
329   }
330
331
332   // Android 4.4 don't accurately give scroll events.
333   // To fix this problem, we setup a fake scroll event. We say:
334   // > If a scroll or touchmove event has happened in the last DELAY milliseconds, 
335   //   then send a `$scroll` event every animationFrame.
336   // Additionally, we add $scrollstart and $scrollend events.
337   function setupAugmentedScrollEvents(element) {
338     var SCROLL_END_DELAY = 200;
339     var isScrolling;
340     var lastScrollTime;
341     element.on('scroll touchmove', function() {
342       if (!isScrolling) {
343         isScrolling = true;
344         $$rAF.throttle(loopScrollEvent);
345         element.triggerHandler('$scrollstart');
346       }
347       element.triggerHandler('$scroll');
348       lastScrollTime = +$mdUtil.now();
349     });
350
351     function loopScrollEvent() {
352       if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
353         isScrolling = false;
354         element.triggerHandler('$scrollend');
355       } else {
356         element.triggerHandler('$scroll');
357         $$rAF.throttle(loopScrollEvent);
358       }
359     }
360   }
361
362 }
363
364 ngmaterial.components.sticky = angular.module("material.components.sticky");