Fix weakness causing NPE
[clamp.git] / src / main / resources / META-INF / resources / designer / lib / angular-vs-repeat.js
1 //
2 // Copyright Kamil PÄ™kala http://github.com/kamilkp
3 // Angular Virtual Scroll Repeat v1.0.0-rc5 2014/08/01
4 //
5
6 (function(window, angular){
7         'use strict';
8         /* jshint eqnull:true */
9         /* jshint -W038 */
10
11         // DESCRIPTION:
12         // vsRepeat directive stands for Virtual Scroll Repeat. It turns a standard ngRepeated set of elements in a scrollable container
13         // into a component, where the user thinks he has all the elements rendered and all he needs to do is scroll (without any kind of
14         // pagination - which most users loath) and at the same time the browser isn't overloaded by that many elements/angular bindings etc.
15         // The directive renders only so many elements that can fit into current container's clientHeight/clientWidth.
16
17         // LIMITATIONS:
18         // - current version only supports an Array as a right-hand-side object for ngRepeat
19         // - all rendered elements must have the same height/width or the sizes of the elements must be known up front
20
21         // USAGE:
22         // In order to use the vsRepeat directive you need to place a vs-repeat attribute on a direct parent of an element with ng-repeat
23         // example:
24         // <div vs-repeat>
25         //              <div ng-repeat="item in someArray">
26         //                      <!-- content -->
27         //              </div>
28         // </div>
29         //
30         // You can also measure the single element's height/width (including all paddings and margins), and then speficy it as a value
31         // of the attribute 'vs-repeat'. This can be used if one wants to override the automatically computed element size.
32         // example:
33         // <div vs-repeat="50"> <!-- the specified element height is 50px -->
34         //              <div ng-repeat="item in someArray">
35         //                      <!-- content -->
36         //              </div>
37         // </div>
38         //
39         // IMPORTANT!
40         //
41         // - the vsRepeat directive must be applied to a direct parent of an element with ngRepeat
42         // - the value of vsRepeat attribute is the single element's height/width measured in pixels. If none provided, the directive
43         //              will compute it automatically
44
45         // OPTIONAL PARAMETERS (attributes):
46         // vs-scroll-parent="selector" - selector to the scrollable container. The directive will look for a closest parent matching
47         //                                                              he given selector (defaults to the current element)
48         // vs-horizontal - stack repeated elements horizontally instead of vertically
49         // vs-offset-before="value" - top/left offset in pixels (defaults to 0)
50         // vs-offset-after="value" - bottom/right offset in pixels (defaults to 0)
51         // vs-excess="value" - an integer number representing the number of elements to be rendered outside of the current container's viewport
52         //                                              (defaults to 2)
53         // vs-size-property - a property name of the items in collection that is a number denoting the element size (in pixels)
54         // vs-autoresize - use this attribute without vs-size-property and without specifying element's size. The automatically computed element style will
55         //                              readjust upon window resize if the size is dependable on the viewport size
56
57         // EVENTS:
58         // - 'vsRepeatTrigger' - an event the directive listens for to manually trigger reinitialization
59         // - 'vsRepeatReinitialized' - an event the directive emits upon reinitialization done
60
61         var isMacOS = navigator.appVersion.indexOf('Mac') != -1,
62                 wheelEventName = typeof window.onwheel !== 'undefined' ? 'wheel' : typeof window.onmousewheel !== 'undefined' ? 'mousewheel' : 'DOMMouseScroll',
63                 dde = document.documentElement,
64                 matchingFunction = dde.matches ? 'matches' :
65                                                         dde.matchesSelector ? 'matchesSelector' :
66                                                         dde.webkitMatches ? 'webkitMatches' :
67                                                         dde.webkitMatchesSelector ? 'webkitMatchesSelector' :
68                                                         dde.msMatches ? 'msMatches' :
69                                                         dde.msMatchesSelector ? 'msMatchesSelector' :
70                                                         dde.mozMatches ? 'mozMatches' :
71                                                         dde.mozMatchesSelector ? 'mozMatchesSelector' : null;
72
73         var closestElement = angular.element.prototype.closest || function (selector){
74                 var el = this[0].parentNode;
75                 while(el !== document.documentElement && el != null && !el[matchingFunction](selector)){
76                         el = el.parentNode;
77                 }
78
79                 if(el && el[matchingFunction](selector))
80                         return angular.element(el);
81                 else
82                         return angular.element();
83         };
84
85         angular.module('vs-repeat', []).directive('vsRepeat', ['$compile', function($compile){
86                 return {
87                         restrict: 'A',
88                         scope: true,
89                         require: '?^vsRepeat',
90                         controller: ['$scope', function($scope){
91                                 this.$scrollParent = $scope.$scrollParent;
92                                 this.$fillElement = $scope.$fillElement;
93                         }],
94                         compile: function($element, $attrs){
95                                 var ngRepeatChild = $element.children().eq(0),
96                                         ngRepeatExpression = ngRepeatChild.attr('ng-repeat'),
97                                         childCloneHtml = ngRepeatChild[0].outerHTML,
98                                         expressionMatches = /^\s*(\S+)\s+in\s+([\S\s]+?)(track\s+by\s+\S+)?$/.exec(ngRepeatExpression),
99                                         lhs = expressionMatches[1],
100                                         rhs = expressionMatches[2],
101                                         rhsSuffix = expressionMatches[3],
102                                         collectionName = '$vs_collection',
103                                         attributesDictionary = {
104                                                 'vsRepeat': 'elementSize',
105                                                 'vsOffsetBefore': 'offsetBefore',
106                                                 'vsOffsetAfter': 'offsetAfter',
107                                                 'vsExcess': 'excess'
108                                         };
109
110                                 $element.empty();
111                                 if(!window.getComputedStyle || window.getComputedStyle($element[0]).position !== 'absolute')
112                                         $element.css('position', 'relative');
113                                 return {
114                                         pre: function($scope, $element, $attrs, $ctrl){
115                                                 var childClone = angular.element(childCloneHtml),
116                                                         originalCollection = [],
117                                                         originalLength,
118                                                         $$horizontal = typeof $attrs.vsHorizontal !== "undefined",
119                                                         $wheelHelper,
120                                                         $fillElement,
121                                                         autoSize = !$attrs.vsRepeat,
122                                                         sizesPropertyExists = !!$attrs.vsSizeProperty,
123                                                         $scrollParent = $attrs.vsScrollParent ? closestElement.call($element, $attrs.vsScrollParent) : $element,
124                                                         positioningPropertyTransform = $$horizontal ? 'translateX' : 'translateY',
125                                                         positioningProperty = $$horizontal ? 'left' : 'top',
126
127                                                         clientSize =  $$horizontal ? 'clientWidth' : 'clientHeight',
128                                                         offsetSize =  $$horizontal ? 'offsetWidth' : 'offsetHeight',
129                                                         scrollPos =  $$horizontal ? 'scrollLeft' : 'scrollTop';
130
131                                                 if($scrollParent.length === 0) throw 'Specified scroll parent selector did not match any element';
132                                                 $scope.$scrollParent = $scrollParent;
133
134                                                 if(sizesPropertyExists) $scope.sizesCumulative = [];
135
136                                                 //initial defaults
137                                                 $scope.elementSize = $scrollParent[0][clientSize] || 50;
138                                                 $scope.offsetBefore = 0;
139                                                 $scope.offsetAfter = 0;
140                                                 $scope.excess = 2;
141
142                                                 Object.keys(attributesDictionary).forEach(function(key){
143                                                         if($attrs[key]){
144                                                                 $attrs.$observe(key, function(value){
145                                                                         $scope[attributesDictionary[key]] = +value;
146                                                                         reinitialize();
147                                                                 });
148                                                         }
149                                                 });
150
151
152                                                 $scope.$watchCollection(rhs, function(coll){
153                                                         originalCollection = coll || [];
154                                                         refresh();
155                                                 });
156
157                                                 function refresh(){
158                                                         if(!originalCollection || originalCollection.length < 1){
159                                                                 $scope[collectionName] = [];
160                                                                 originalLength = 0;
161                                                                 resizeFillElement(0);
162                                                                 $scope.sizesCumulative = [0];
163                                                                 return;
164                                                         }
165                                                         else{
166                                                                 originalLength = originalCollection.length;
167                                                                 if(sizesPropertyExists){
168                                                                         $scope.sizes = originalCollection.map(function(item){
169                                                                                 return item[$attrs.vsSizeProperty];
170                                                                         });
171                                                                         var sum = 0;
172                                                                         $scope.sizesCumulative = $scope.sizes.map(function(size){
173                                                                                 var res = sum;
174                                                                                 sum += size;
175                                                                                 return res;
176                                                                         });
177                                                                         $scope.sizesCumulative.push(sum);
178                                                                 }
179                                                                 setAutoSize();
180                                                         }
181
182                                                         reinitialize();
183                                                 }
184
185                                                 function setAutoSize(){
186                                                         if(autoSize){
187                                                                 $scope.$$postDigest(function(){
188                                                                         if($element[0].offsetHeight || $element[0].offsetWidth){ // element is visible
189                                                                                 var children = $element.children(),
190                                                                                         i = 0;
191                                                                                 while(i < children.length){
192                                                                                         if(children[i].attributes['ng-repeat'] != null){
193                                                                                                 if(children[i][offsetSize]){
194                                                                                                         $scope.elementSize = children[i][offsetSize];
195                                                                                                         reinitialize();
196                                                                                                         autoSize = false;
197                                                                                                         if($scope.$root && !$scope.$root.$$phase)
198                                                                                                                 $scope.$apply();
199                                                                                                 }
200                                                                                                 break;
201                                                                                         }
202                                                                                         i++;
203                                                                                 }
204                                                                         }
205                                                                         else{
206                                                                                 var dereg = $scope.$watch(function(){
207                                                                                         if($element[0].offsetHeight || $element[0].offsetWidth){
208                                                                                                 dereg();
209                                                                                                 setAutoSize();
210                                                                                         }
211                                                                                 });
212                                                                         }
213                                                                 });
214                                                         }
215                                                 }
216
217                                                 childClone.attr('ng-repeat', lhs + ' in ' + collectionName + (rhsSuffix ? ' ' + rhsSuffix : ''))
218                                                                 .addClass('vs-repeat-repeated-element');
219
220                                                 var offsetCalculationString = sizesPropertyExists ?
221                                                         '(sizesCumulative[$index + startIndex] + offsetBefore)' :
222                                                         '(($index + startIndex) * elementSize + offsetBefore)';
223
224                                                 if(typeof document.documentElement.style.transform !== "undefined"){ // browser supports transform css property
225                                                         childClone.attr('ng-style', '{ "transform": "' + positioningPropertyTransform + '(" + ' + offsetCalculationString + ' + "px)"}');
226                                                 }
227                                                 else if(typeof document.documentElement.style.webkitTransform !== "undefined"){ // browser supports -webkit-transform css property
228                                                         childClone.attr('ng-style', '{ "-webkit-transform": "' + positioningPropertyTransform + '(" + ' + offsetCalculationString + ' + "px)"}');
229                                                 }
230                                                 else{
231                                                         childClone.attr('ng-style', '{' + positioningProperty + ': ' + offsetCalculationString + ' + "px"}');
232                                                 }
233
234                                                 $compile(childClone)($scope);
235                                                 $element.append(childClone);
236
237                                                 $fillElement = angular.element('<div class="vs-repeat-fill-element"></div>')
238                                                         .css({
239                                                                 'position':'relative',
240                                                                 'min-height': '100%',
241                                                                 'min-width': '100%'
242                                                         });
243                                                 $element.append($fillElement);
244                                                 $compile($fillElement)($scope);
245                                                 $scope.$fillElement = $fillElement;
246
247                                                 var _prevMouse = {};
248                                                 if(isMacOS){
249                                                         $wheelHelper = angular.element('<div class="vs-repeat-wheel-helper"></div>')
250                                                                 .on(wheelEventName, function(e){
251                                                                         e.preventDefault();
252                                                                         e.stopPropagation();
253                                                                         if(e.originalEvent) e = e.originalEvent;
254                                                                         $scrollParent[0].scrollLeft += (e.deltaX || -e.wheelDeltaX);
255                                                                         $scrollParent[0].scrollTop += (e.deltaY || -e.wheelDeltaY);
256                                                                 }).on('mousemove', function(e){
257                                                                         if(_prevMouse.x !== e.clientX || _prevMouse.y !== e.clientY)
258                                                                                 angular.element(this).css('display', 'none');
259                                                                         _prevMouse = {
260                                                                                 x: e.clientX,
261                                                                                 y: e.clientY
262                                                                         };
263                                                                 }).css('display', 'none');
264                                                         $fillElement.append($wheelHelper);
265                                                 }
266
267                                                 $scope.startIndex = 0;
268                                                 $scope.endIndex = 0;
269
270                                                 $scrollParent.on('scroll', function scrollHandler(e){
271                                                         if(updateInnerCollection())
272                                                                 $scope.$apply();
273                                                 });
274
275                                                 if(isMacOS){
276                                                         $scrollParent.on(wheelEventName, wheelHandler);
277                                                 }
278                                                 function wheelHandler(e){
279                                                         var elem = e.currentTarget;
280                                                         if(elem.scrollWidth > elem.clientWidth || elem.scrollHeight > elem.clientHeight)
281                                                                 $wheelHelper.css('display', 'block');
282                                                 }
283
284                                                 function onWindowResize(){
285                                                         if(typeof $attrs.vsAutoresize !== 'undefined'){
286                                                                 autoSize = true;
287                                                                 setAutoSize();
288                                                                 if($scope.$root && !$scope.$root.$$phase)
289                                                                         $scope.$apply();
290                                                         }
291                                                         if(updateInnerCollection())
292                                                                 $scope.$apply();
293                                                 }
294
295                                                 angular.element(window).on('resize', onWindowResize);
296                                                 $scope.$on('$destroy', function(){
297                                                         angular.element(window).off('resize', onWindowResize);
298                                                 });
299
300                                                 $scope.$on('vsRepeatTrigger', refresh);
301                                                 $scope.$on('vsRepeatResize', function(){
302                                                         autoSize = true;
303                                                         setAutoSize();
304                                                 });
305
306                                                 var _prevStartIndex,
307                                                         _prevEndIndex;
308                                                 function reinitialize(){
309                                                         _prevStartIndex = void 0;
310                                                         _prevEndIndex = void 0;
311                                                         updateInnerCollection();
312                                                         resizeFillElement(sizesPropertyExists ?
313                                                                                                 $scope.sizesCumulative[originalLength] :
314                                                                                                 $scope.elementSize*originalLength
315                                                                                         );
316                                                         $scope.$emit('vsRepeatReinitialized');
317                                                 }
318
319                                                 function resizeFillElement(size){
320                                                         if($$horizontal){
321                                                                 $fillElement.css({
322                                                                         'width': $scope.offsetBefore + size + $scope.offsetAfter + 'px',
323                                                                         'height': '100%'
324                                                                 });
325                                                                 if($ctrl && $ctrl.$fillElement){
326                                                                         var referenceElement = $ctrl.$fillElement[0].parentNode.querySelector('[ng-repeat]');
327                                                                         if(referenceElement)
328                                                                                 $ctrl.$fillElement.css({
329                                                                                         'width': referenceElement.scrollWidth + 'px'
330                                                                                 });
331                                                                 }
332                                                         }
333                                                         else{
334                                                                 $fillElement.css({
335                                                                         'height': $scope.offsetBefore + size + $scope.offsetAfter + 'px',
336                                                                         'width': '100%'
337                                                                 });
338                                                                 if($ctrl && $ctrl.$fillElement){
339                                                                         referenceElement = $ctrl.$fillElement[0].parentNode.querySelector('[ng-repeat]');
340                                                                         if(referenceElement)
341                                                                                 $ctrl.$fillElement.css({
342                                                                                         'height': referenceElement.scrollHeight + 'px'
343                                                                                 });
344                                                                 }
345                                                         }
346                                                 }
347
348                                                 var _prevClientSize;
349                                                 function reinitOnClientHeightChange(){
350                                                         var ch = $scrollParent[0][clientSize];
351                                                         if(ch !== _prevClientSize){
352                                                                 reinitialize();
353                                                                 if($scope.$root && !$scope.$root.$$phase)
354                                                                         $scope.$apply();
355                                                         }
356                                                         _prevClientSize = ch;
357                                                 }
358
359                                                 $scope.$watch(function(){
360                                                         if(typeof window.requestAnimationFrame === "function")
361                                                                 window.requestAnimationFrame(reinitOnClientHeightChange);
362                                                         else
363                                                                 reinitOnClientHeightChange();
364                                                 });
365
366                                                 function updateInnerCollection(){
367                                                         if(sizesPropertyExists){
368                                                                 $scope.startIndex = 0;
369                                                                 while($scope.sizesCumulative[$scope.startIndex] < $scrollParent[0][scrollPos] - $scope.offsetBefore)
370                                                                         $scope.startIndex++;
371                                                                 if($scope.startIndex > 0) $scope.startIndex--;
372
373                                                                 $scope.endIndex = $scope.startIndex;
374                                                                 while($scope.sizesCumulative[$scope.endIndex] < $scrollParent[0][scrollPos] - $scope.offsetBefore + $scrollParent[0][clientSize])
375                                                                         $scope.endIndex++;
376                                                         }
377                                                         else{
378                                                                 $scope.startIndex = Math.max(
379                                                                         Math.floor(
380                                                                                 ($scrollParent[0][scrollPos] - $scope.offsetBefore) / $scope.elementSize + $scope.excess/2
381                                                                         ) - $scope.excess,
382                                                                         0
383                                                                 );
384
385                                                                 $scope.endIndex = Math.min(
386                                                                         $scope.startIndex + Math.ceil(
387                                                                                 $scrollParent[0][clientSize] / $scope.elementSize
388                                                                         ) + $scope.excess,
389                                                                         originalLength
390                                                                 );
391                                                         }
392
393
394                                                         var digestRequired = $scope.startIndex !== _prevStartIndex || $scope.endIndex !== _prevEndIndex;
395
396                                                         if(digestRequired)
397                                                                 $scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex);
398
399                                                         _prevStartIndex = $scope.startIndex;
400                                                         _prevEndIndex = $scope.endIndex;
401
402                                                         return digestRequired;
403                                                 }
404                                         }
405                                 };
406                         }
407                 };
408         }]);
409
410         angular.element(document.head).append([
411                 '<style>' +
412                 '.vs-repeat-wheel-helper{' +
413                         'position: absolute;' +
414                         'top: 0;' +
415                         'bottom: 0;' +
416                         'left: 0;' +
417                         'right: 0;' +
418                         'z-index: 99999;' +
419                         'background: rgba(0, 0, 0, 0);' +
420                 '}' +
421                 '.vs-repeat-repeated-element{' +
422                         'position: absolute;' +
423                         'z-index: 1;' +
424                 '}' +
425                 '</style>'
426         ].join(''));
427 })(window, window.angular);