Merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:~maas-committers/maas/trunk
- vs-repeat-ui-improvement
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 6088 |
Proposed branch: | lp:~ricgard/maas/vs-repeat-ui-improvement |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
771 lines (+630/-10) 10 files modified
src/maasserver/static/js/angular/3rdparty/vs-repeat.js (+608/-0) src/maasserver/static/js/angular/maas.js (+2/-1) src/maasserver/static/partials/dashboard.html (+3/-1) src/maasserver/static/partials/machines-table.html (+1/-1) src/maasserver/static/partials/networks-list.html (+2/-2) src/maasserver/static/partials/node-events.html (+1/-1) src/maasserver/static/partials/nodes-list.html (+3/-3) src/maasserver/static/partials/pods-list.html (+1/-1) src/maasserver/templates/maasserver/js-conf.html (+3/-0) src/maasserver/views/combo.py (+6/-0) |
To merge this branch: | bzr merge lp:~ricgard/maas/vs-repeat-ui-improvement |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+325562@code.launchpad.net |
Commit message
Created vertical scrolling repeat to improve the loading times of heavy table content pages such as device discovery
Description of the change
Work done
=======
- Added the vs-repeat angular module to MAAS. Module details and code can be found here https:/
- Applied the vs-repeat directive to the following pages:
Device discovery, Node listing, Device listing, Controller listing, Pod listing, Subnet listing, Node events page
How to test
=======
- Pull down the branch and run 'make' then 'make sampledata'
- Go to http://
- View the device discovery page and attempt to rapidly scroll the page. The directive should pull in the table row content and improve the loading time on the page.
- Test in a similar way on other pages such as node events
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:maas failed. Below is the output from the failed tests.
Get:1 http://
Hit:2 http://
Get:3 http://
Get:4 http://
Get:5 http://
Get:6 http://
Fetched 1,112 kB in 0s (2,080 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+
python-formencode is already the n...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Hit:2 http://
Hit:3 http://
Hit:4 http://
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18-1).
python-netifaces is already the newest version (0.10.4-0.1build2).
python-psycopg2 is already the newest version (2.6.1-1b...
Preview Diff
1 | === added file 'src/maasserver/static/js/angular/3rdparty/vs-repeat.js' |
2 | --- src/maasserver/static/js/angular/3rdparty/vs-repeat.js 1970-01-01 00:00:00 +0000 |
3 | +++ src/maasserver/static/js/angular/3rdparty/vs-repeat.js 2017-06-15 11:16:28 +0000 |
4 | @@ -0,0 +1,608 @@ |
5 | +// |
6 | +// Copyright Kamil Pękala http://github.com/kamilkp |
7 | +// Angular Virtual Scroll Repeat v1.1.7 2016/03/08 |
8 | +// |
9 | + |
10 | +(function(window, angular) { |
11 | + 'use strict'; |
12 | + /* jshint eqnull:true */ |
13 | + /* jshint -W038 */ |
14 | + |
15 | + // DESCRIPTION: |
16 | + // vsRepeat directive stands for Virtual Scroll Repeat. It turns a standard ngRepeated set of elements in a scrollable container |
17 | + // 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 |
18 | + // pagination - which most users loath) and at the same time the browser isn't overloaded by that many elements/angular bindings etc. |
19 | + // The directive renders only so many elements that can fit into current container's clientHeight/clientWidth. |
20 | + |
21 | + // LIMITATIONS: |
22 | + // - current version only supports an Array as a right-hand-side object for ngRepeat |
23 | + // - all rendered elements must have the same height/width or the sizes of the elements must be known up front |
24 | + |
25 | + // USAGE: |
26 | + // 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 |
27 | + // example: |
28 | + // <div vs-repeat> |
29 | + // <div ng-repeat="item in someArray"> |
30 | + // <!-- content --> |
31 | + // </div> |
32 | + // </div> |
33 | + // |
34 | + // or: |
35 | + // <div vs-repeat> |
36 | + // <div ng-repeat-start="item in someArray"> |
37 | + // <!-- content --> |
38 | + // </div> |
39 | + // <div> |
40 | + // <!-- something in the middle --> |
41 | + // </div> |
42 | + // <div ng-repeat-end> |
43 | + // <!-- content --> |
44 | + // </div> |
45 | + // </div> |
46 | + // |
47 | + // You can also measure the single element's height/width (including all paddings and margins), and then speficy it as a value |
48 | + // of the attribute 'vs-repeat'. This can be used if one wants to override the automatically computed element size. |
49 | + // example: |
50 | + // <div vs-repeat="50"> <!-- the specified element height is 50px --> |
51 | + // <div ng-repeat="item in someArray"> |
52 | + // <!-- content --> |
53 | + // </div> |
54 | + // </div> |
55 | + // |
56 | + // IMPORTANT! |
57 | + // |
58 | + // - the vsRepeat directive must be applied to a direct parent of an element with ngRepeat |
59 | + // - the value of vsRepeat attribute is the single element's height/width measured in pixels. If none provided, the directive |
60 | + // will compute it automatically |
61 | + |
62 | + // OPTIONAL PARAMETERS (attributes): |
63 | + // vs-repeat-container="selector" - selector for element containing ng-repeat. (defaults to the current element) |
64 | + // vs-scroll-parent="selector" - selector to the scrollable container. The directive will look for a closest parent matching |
65 | + // the given selector (defaults to the current element) |
66 | + // vs-horizontal - stack repeated elements horizontally instead of vertically |
67 | + // vs-offset-before="value" - top/left offset in pixels (defaults to 0) |
68 | + // vs-offset-after="value" - bottom/right offset in pixels (defaults to 0) |
69 | + // vs-excess="value" - an integer number representing the number of elements to be rendered outside of the current container's viewport |
70 | + // (defaults to 2) |
71 | + // vs-size - a property name of the items in collection that is a number denoting the element size (in pixels) |
72 | + // vs-autoresize - use this attribute without vs-size and without specifying element's size. The automatically computed element style will |
73 | + // readjust upon window resize if the size is dependable on the viewport size |
74 | + // vs-scrolled-to-end="callback" - callback will be called when the last item of the list is rendered |
75 | + // vs-scrolled-to-end-offset="integer" - set this number to trigger the scrolledToEnd callback n items before the last gets rendered |
76 | + // vs-scrolled-to-beginning="callback" - callback will be called when the first item of the list is rendered |
77 | + // vs-scrolled-to-beginning-offset="integer" - set this number to trigger the scrolledToBeginning callback n items before the first gets rendered |
78 | + |
79 | + // EVENTS: |
80 | + // - 'vsRepeatTrigger' - an event the directive listens for to manually trigger reinitialization |
81 | + // - 'vsRepeatReinitialized' - an event the directive emits upon reinitialization done |
82 | + |
83 | + var dde = document.documentElement, |
84 | + matchingFunction = dde.matches ? 'matches' : |
85 | + dde.matchesSelector ? 'matchesSelector' : |
86 | + dde.webkitMatches ? 'webkitMatches' : |
87 | + dde.webkitMatchesSelector ? 'webkitMatchesSelector' : |
88 | + dde.msMatches ? 'msMatches' : |
89 | + dde.msMatchesSelector ? 'msMatchesSelector' : |
90 | + dde.mozMatches ? 'mozMatches' : |
91 | + dde.mozMatchesSelector ? 'mozMatchesSelector' : null; |
92 | + |
93 | + var closestElement = angular.element.prototype.closest || function (selector) { |
94 | + var el = this[0].parentNode; |
95 | + while (el !== document.documentElement && el != null && !el[matchingFunction](selector)) { |
96 | + el = el.parentNode; |
97 | + } |
98 | + |
99 | + if (el && el[matchingFunction](selector)) { |
100 | + return angular.element(el); |
101 | + } |
102 | + else { |
103 | + return angular.element(); |
104 | + } |
105 | + }; |
106 | + |
107 | + function getWindowScroll() { |
108 | + if ('pageYOffset' in window) { |
109 | + return { |
110 | + scrollTop: pageYOffset, |
111 | + scrollLeft: pageXOffset |
112 | + }; |
113 | + } |
114 | + else { |
115 | + var sx, sy, d = document, r = d.documentElement, b = d.body; |
116 | + sx = r.scrollLeft || b.scrollLeft || 0; |
117 | + sy = r.scrollTop || b.scrollTop || 0; |
118 | + return { |
119 | + scrollTop: sy, |
120 | + scrollLeft: sx |
121 | + }; |
122 | + } |
123 | + } |
124 | + |
125 | + function getClientSize(element, sizeProp) { |
126 | + if (element === window) { |
127 | + return sizeProp === 'clientWidth' ? window.innerWidth : window.innerHeight; |
128 | + } |
129 | + else { |
130 | + return element[sizeProp]; |
131 | + } |
132 | + } |
133 | + |
134 | + function getScrollPos(element, scrollProp) { |
135 | + return element === window ? getWindowScroll()[scrollProp] : element[scrollProp]; |
136 | + } |
137 | + |
138 | + function getScrollOffset(vsElement, scrollElement, isHorizontal) { |
139 | + var vsPos = vsElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top']; |
140 | + var scrollPos = scrollElement === window ? 0 : scrollElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top']; |
141 | + var correction = vsPos - scrollPos + |
142 | + (scrollElement === window ? getWindowScroll() : scrollElement)[isHorizontal ? 'scrollLeft' : 'scrollTop']; |
143 | + |
144 | + return correction; |
145 | + } |
146 | + |
147 | + var vsRepeatModule = angular.module('vs-repeat', []).directive('vsRepeat', ['$compile', '$parse', function($compile, $parse) { |
148 | + return { |
149 | + restrict: 'A', |
150 | + scope: true, |
151 | + compile: function($element, $attrs) { |
152 | + var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element, |
153 | + ngRepeatChild = repeatContainer.children().eq(0), |
154 | + ngRepeatExpression, |
155 | + childCloneHtml = ngRepeatChild[0].outerHTML, |
156 | + expressionMatches, |
157 | + lhs, |
158 | + rhs, |
159 | + rhsSuffix, |
160 | + originalNgRepeatAttr, |
161 | + collectionName = '$vs_collection', |
162 | + isNgRepeatStart = false, |
163 | + attributesDictionary = { |
164 | + 'vsRepeat': 'elementSize', |
165 | + 'vsOffsetBefore': 'offsetBefore', |
166 | + 'vsOffsetAfter': 'offsetAfter', |
167 | + 'vsScrolledToEndOffset': 'scrolledToEndOffset', |
168 | + 'vsScrolledToBeginningOffset': 'scrolledToBeginningOffset', |
169 | + 'vsExcess': 'excess' |
170 | + }; |
171 | + |
172 | + if (ngRepeatChild.attr('ng-repeat')) { |
173 | + originalNgRepeatAttr = 'ng-repeat'; |
174 | + ngRepeatExpression = ngRepeatChild.attr('ng-repeat'); |
175 | + } |
176 | + else if (ngRepeatChild.attr('data-ng-repeat')) { |
177 | + originalNgRepeatAttr = 'data-ng-repeat'; |
178 | + ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat'); |
179 | + } |
180 | + else if (ngRepeatChild.attr('ng-repeat-start')) { |
181 | + isNgRepeatStart = true; |
182 | + originalNgRepeatAttr = 'ng-repeat-start'; |
183 | + ngRepeatExpression = ngRepeatChild.attr('ng-repeat-start'); |
184 | + } |
185 | + else if (ngRepeatChild.attr('data-ng-repeat-start')) { |
186 | + isNgRepeatStart = true; |
187 | + originalNgRepeatAttr = 'data-ng-repeat-start'; |
188 | + ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat-start'); |
189 | + } |
190 | + else { |
191 | + throw new Error('angular-vs-repeat: no ng-repeat directive on a child element'); |
192 | + } |
193 | + |
194 | + expressionMatches = /^\s*(\S+)\s+in\s+([\S\s]+?)(track\s+by\s+\S+)?$/.exec(ngRepeatExpression); |
195 | + lhs = expressionMatches[1]; |
196 | + rhs = expressionMatches[2]; |
197 | + rhsSuffix = expressionMatches[3]; |
198 | + |
199 | + if (isNgRepeatStart) { |
200 | + var index = 0; |
201 | + var repeaterElement = repeatContainer.children().eq(0); |
202 | + while(repeaterElement.attr('ng-repeat-end') == null && repeaterElement.attr('data-ng-repeat-end') == null) { |
203 | + index++; |
204 | + repeaterElement = repeatContainer.children().eq(index); |
205 | + childCloneHtml += repeaterElement[0].outerHTML; |
206 | + } |
207 | + } |
208 | + |
209 | + repeatContainer.empty(); |
210 | + return { |
211 | + pre: function($scope, $element, $attrs) { |
212 | + var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element, |
213 | + childClone = angular.element(childCloneHtml), |
214 | + childTagName = childClone[0].tagName.toLowerCase(), |
215 | + originalCollection = [], |
216 | + originalLength, |
217 | + $$horizontal = typeof $attrs.vsHorizontal !== 'undefined', |
218 | + $beforeContent = angular.element('<' + childTagName + ' class="vs-repeat-before-content"></' + childTagName + '>'), |
219 | + $afterContent = angular.element('<' + childTagName + ' class="vs-repeat-after-content"></' + childTagName + '>'), |
220 | + autoSize = !$attrs.vsRepeat, |
221 | + sizesPropertyExists = !!$attrs.vsSize || !!$attrs.vsSizeProperty, |
222 | + $scrollParent = $attrs.vsScrollParent ? |
223 | + $attrs.vsScrollParent === 'window' ? angular.element(window) : |
224 | + closestElement.call(repeatContainer, $attrs.vsScrollParent) : repeatContainer, |
225 | + $$options = 'vsOptions' in $attrs ? $scope.$eval($attrs.vsOptions) : {}, |
226 | + clientSize = $$horizontal ? 'clientWidth' : 'clientHeight', |
227 | + offsetSize = $$horizontal ? 'offsetWidth' : 'offsetHeight', |
228 | + scrollPos = $$horizontal ? 'scrollLeft' : 'scrollTop'; |
229 | + |
230 | + $scope.totalSize = 0; |
231 | + if (!('vsSize' in $attrs) && 'vsSizeProperty' in $attrs) { |
232 | + console.warn('vs-size-property attribute is deprecated. Please use vs-size attribute which also accepts angular expressions.'); |
233 | + } |
234 | + |
235 | + if ($scrollParent.length === 0) { |
236 | + throw 'Specified scroll parent selector did not match any element'; |
237 | + } |
238 | + $scope.$scrollParent = $scrollParent; |
239 | + |
240 | + if (sizesPropertyExists) { |
241 | + $scope.sizesCumulative = []; |
242 | + } |
243 | + |
244 | + //initial defaults |
245 | + $scope.elementSize = (+$attrs.vsRepeat) || getClientSize($scrollParent[0], clientSize) || 50; |
246 | + $scope.offsetBefore = 0; |
247 | + $scope.offsetAfter = 0; |
248 | + $scope.excess = 2; |
249 | + |
250 | + if ($$horizontal) { |
251 | + $beforeContent.css('height', '100%'); |
252 | + $afterContent.css('height', '100%'); |
253 | + } |
254 | + else { |
255 | + $beforeContent.css('width', '100%'); |
256 | + $afterContent.css('width', '100%'); |
257 | + } |
258 | + |
259 | + Object.keys(attributesDictionary).forEach(function(key) { |
260 | + if ($attrs[key]) { |
261 | + $attrs.$observe(key, function(value) { |
262 | + // '+' serves for getting a number from the string as the attributes are always strings |
263 | + $scope[attributesDictionary[key]] = +value; |
264 | + reinitialize(); |
265 | + }); |
266 | + } |
267 | + }); |
268 | + |
269 | + |
270 | + $scope.$watchCollection(rhs, function(coll) { |
271 | + originalCollection = coll || []; |
272 | + refresh(); |
273 | + }); |
274 | + |
275 | + function refresh() { |
276 | + if (!originalCollection || originalCollection.length < 1) { |
277 | + $scope[collectionName] = []; |
278 | + originalLength = 0; |
279 | + $scope.sizesCumulative = [0]; |
280 | + } |
281 | + else { |
282 | + originalLength = originalCollection.length; |
283 | + if (sizesPropertyExists) { |
284 | + $scope.sizes = originalCollection.map(function(item) { |
285 | + var s = $scope.$new(false); |
286 | + angular.extend(s, item); |
287 | + s[lhs] = item; |
288 | + var size = ($attrs.vsSize || $attrs.vsSizeProperty) ? |
289 | + s.$eval($attrs.vsSize || $attrs.vsSizeProperty) : |
290 | + $scope.elementSize; |
291 | + s.$destroy(); |
292 | + return size; |
293 | + }); |
294 | + var sum = 0; |
295 | + $scope.sizesCumulative = $scope.sizes.map(function(size) { |
296 | + var res = sum; |
297 | + sum += size; |
298 | + return res; |
299 | + }); |
300 | + $scope.sizesCumulative.push(sum); |
301 | + } |
302 | + else { |
303 | + setAutoSize(); |
304 | + } |
305 | + } |
306 | + |
307 | + reinitialize(); |
308 | + } |
309 | + |
310 | + function setAutoSize() { |
311 | + if (autoSize) { |
312 | + $scope.$$postDigest(function() { |
313 | + if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) { // element is visible |
314 | + var children = repeatContainer.children(), |
315 | + i = 0, |
316 | + gotSomething = false, |
317 | + insideStartEndSequence = false; |
318 | + |
319 | + while (i < children.length) { |
320 | + if (children[i].attributes[originalNgRepeatAttr] != null || insideStartEndSequence) { |
321 | + if (!gotSomething) { |
322 | + $scope.elementSize = 0; |
323 | + } |
324 | + |
325 | + gotSomething = true; |
326 | + if (children[i][offsetSize]) { |
327 | + $scope.elementSize += children[i][offsetSize]; |
328 | + } |
329 | + |
330 | + if (isNgRepeatStart) { |
331 | + if (children[i].attributes['ng-repeat-end'] != null || children[i].attributes['data-ng-repeat-end'] != null) { |
332 | + break; |
333 | + } |
334 | + else { |
335 | + insideStartEndSequence = true; |
336 | + } |
337 | + } |
338 | + else { |
339 | + break; |
340 | + } |
341 | + } |
342 | + i++; |
343 | + } |
344 | + |
345 | + if (gotSomething) { |
346 | + reinitialize(); |
347 | + autoSize = false; |
348 | + if ($scope.$root && !$scope.$root.$$phase) { |
349 | + $scope.$apply(); |
350 | + } |
351 | + } |
352 | + } |
353 | + else { |
354 | + var dereg = $scope.$watch(function() { |
355 | + if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) { |
356 | + dereg(); |
357 | + setAutoSize(); |
358 | + } |
359 | + }); |
360 | + } |
361 | + }); |
362 | + } |
363 | + } |
364 | + |
365 | + function getLayoutProp() { |
366 | + var layoutPropPrefix = childTagName === 'tr' ? '' : 'min-'; |
367 | + var layoutProp = $$horizontal ? layoutPropPrefix + 'width' : layoutPropPrefix + 'height'; |
368 | + return layoutProp; |
369 | + } |
370 | + |
371 | + childClone.eq(0).attr(originalNgRepeatAttr, lhs + ' in ' + collectionName + (rhsSuffix ? ' ' + rhsSuffix : '')); |
372 | + childClone.addClass('vs-repeat-repeated-element'); |
373 | + |
374 | + repeatContainer.append($beforeContent); |
375 | + repeatContainer.append(childClone); |
376 | + $compile(childClone)($scope); |
377 | + repeatContainer.append($afterContent); |
378 | + |
379 | + $scope.startIndex = 0; |
380 | + $scope.endIndex = 0; |
381 | + |
382 | + function scrollHandler() { |
383 | + if (updateInnerCollection()) { |
384 | + $scope.$digest(); |
385 | + } |
386 | + } |
387 | + |
388 | + $scrollParent.on('scroll', scrollHandler); |
389 | + |
390 | + function onWindowResize() { |
391 | + if (typeof $attrs.vsAutoresize !== 'undefined') { |
392 | + autoSize = true; |
393 | + setAutoSize(); |
394 | + if ($scope.$root && !$scope.$root.$$phase) { |
395 | + $scope.$apply(); |
396 | + } |
397 | + } |
398 | + if (updateInnerCollection()) { |
399 | + $scope.$apply(); |
400 | + } |
401 | + } |
402 | + |
403 | + angular.element(window).on('resize', onWindowResize); |
404 | + $scope.$on('$destroy', function() { |
405 | + angular.element(window).off('resize', onWindowResize); |
406 | + $scrollParent.off('scroll', scrollHandler); |
407 | + }); |
408 | + |
409 | + $scope.$on('vsRepeatTrigger', refresh); |
410 | + |
411 | + $scope.$on('vsRepeatResize', function() { |
412 | + autoSize = true; |
413 | + setAutoSize(); |
414 | + }); |
415 | + |
416 | + var _prevStartIndex, |
417 | + _prevEndIndex, |
418 | + _minStartIndex, |
419 | + _maxEndIndex; |
420 | + |
421 | + $scope.$on('vsRenderAll', function() {//e , quantum) { |
422 | + if($$options.latch) { |
423 | + setTimeout(function() { |
424 | + // var __endIndex = Math.min($scope.endIndex + (quantum || 1), originalLength); |
425 | + var __endIndex = originalLength; |
426 | + _maxEndIndex = Math.max(__endIndex, _maxEndIndex); |
427 | + $scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex; |
428 | + $scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex); |
429 | + _prevEndIndex = $scope.endIndex; |
430 | + |
431 | + $scope.$$postDigest(function() { |
432 | + $beforeContent.css(getLayoutProp(), 0); |
433 | + $afterContent.css(getLayoutProp(), 0); |
434 | + }); |
435 | + |
436 | + $scope.$apply(function() { |
437 | + $scope.$emit('vsRenderAllDone'); |
438 | + }); |
439 | + }); |
440 | + } |
441 | + }); |
442 | + |
443 | + function reinitialize() { |
444 | + _prevStartIndex = void 0; |
445 | + _prevEndIndex = void 0; |
446 | + _minStartIndex = originalLength; |
447 | + _maxEndIndex = 0; |
448 | + updateTotalSize(sizesPropertyExists ? |
449 | + $scope.sizesCumulative[originalLength] : |
450 | + $scope.elementSize * originalLength |
451 | + ); |
452 | + updateInnerCollection(); |
453 | + |
454 | + $scope.$emit('vsRepeatReinitialized', $scope.startIndex, $scope.endIndex); |
455 | + } |
456 | + |
457 | + function updateTotalSize(size) { |
458 | + $scope.totalSize = $scope.offsetBefore + size + $scope.offsetAfter; |
459 | + } |
460 | + |
461 | + var _prevClientSize; |
462 | + function reinitOnClientHeightChange() { |
463 | + var ch = getClientSize($scrollParent[0], clientSize); |
464 | + if (ch !== _prevClientSize) { |
465 | + reinitialize(); |
466 | + if ($scope.$root && !$scope.$root.$$phase) { |
467 | + $scope.$apply(); |
468 | + } |
469 | + } |
470 | + _prevClientSize = ch; |
471 | + } |
472 | + |
473 | + $scope.$watch(function() { |
474 | + if (typeof window.requestAnimationFrame === 'function') { |
475 | + window.requestAnimationFrame(reinitOnClientHeightChange); |
476 | + } |
477 | + else { |
478 | + reinitOnClientHeightChange(); |
479 | + } |
480 | + }); |
481 | + |
482 | + function updateInnerCollection() { |
483 | + var $scrollPosition = getScrollPos($scrollParent[0], scrollPos); |
484 | + var $clientSize = getClientSize($scrollParent[0], clientSize); |
485 | + |
486 | + var scrollOffset = repeatContainer[0] === $scrollParent[0] ? 0 : getScrollOffset( |
487 | + repeatContainer[0], |
488 | + $scrollParent[0], |
489 | + $$horizontal |
490 | + ); |
491 | + |
492 | + var __startIndex = $scope.startIndex; |
493 | + var __endIndex = $scope.endIndex; |
494 | + |
495 | + if (sizesPropertyExists) { |
496 | + __startIndex = 0; |
497 | + while ($scope.sizesCumulative[__startIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset) { |
498 | + __startIndex++; |
499 | + } |
500 | + if (__startIndex > 0) { __startIndex--; } |
501 | + |
502 | + // Adjust the start index according to the excess |
503 | + __startIndex = Math.max( |
504 | + Math.floor(__startIndex - $scope.excess / 2), |
505 | + 0 |
506 | + ); |
507 | + |
508 | + __endIndex = __startIndex; |
509 | + while ($scope.sizesCumulative[__endIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset + $clientSize) { |
510 | + __endIndex++; |
511 | + } |
512 | + |
513 | + // Adjust the end index according to the excess |
514 | + __endIndex = Math.min( |
515 | + Math.ceil(__endIndex + $scope.excess / 2), |
516 | + originalLength |
517 | + ); |
518 | + } |
519 | + else { |
520 | + __startIndex = Math.max( |
521 | + Math.floor( |
522 | + ($scrollPosition - $scope.offsetBefore - scrollOffset) / $scope.elementSize |
523 | + ) - $scope.excess / 2, |
524 | + 0 |
525 | + ); |
526 | + |
527 | + __endIndex = Math.min( |
528 | + __startIndex + Math.ceil( |
529 | + $clientSize / $scope.elementSize |
530 | + ) + $scope.excess, |
531 | + originalLength |
532 | + ); |
533 | + } |
534 | + |
535 | + _minStartIndex = Math.min(__startIndex, _minStartIndex); |
536 | + _maxEndIndex = Math.max(__endIndex, _maxEndIndex); |
537 | + |
538 | + $scope.startIndex = $$options.latch ? _minStartIndex : __startIndex; |
539 | + $scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex; |
540 | + |
541 | + var digestRequired = false; |
542 | + if (_prevStartIndex == null) { |
543 | + digestRequired = true; |
544 | + } |
545 | + else if (_prevEndIndex == null) { |
546 | + digestRequired = true; |
547 | + } |
548 | + |
549 | + if (!digestRequired) { |
550 | + if ($$options.hunked) { |
551 | + if (Math.abs($scope.startIndex - _prevStartIndex) >= $scope.excess / 2 || |
552 | + ($scope.startIndex === 0 && _prevStartIndex !== 0)) { |
553 | + digestRequired = true; |
554 | + } |
555 | + else if (Math.abs($scope.endIndex - _prevEndIndex) >= $scope.excess / 2 || |
556 | + ($scope.endIndex === originalLength && _prevEndIndex !== originalLength)) { |
557 | + digestRequired = true; |
558 | + } |
559 | + } |
560 | + else { |
561 | + digestRequired = $scope.startIndex !== _prevStartIndex || |
562 | + $scope.endIndex !== _prevEndIndex; |
563 | + } |
564 | + } |
565 | + |
566 | + if (digestRequired) { |
567 | + $scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex); |
568 | + |
569 | + // Emit the event |
570 | + $scope.$emit('vsRepeatInnerCollectionUpdated', $scope.startIndex, $scope.endIndex, _prevStartIndex, _prevEndIndex); |
571 | + var triggerIndex; |
572 | + if ($attrs.vsScrolledToEnd) { |
573 | + triggerIndex = originalCollection.length - ($scope.scrolledToEndOffset || 0); |
574 | + if (($scope.endIndex >= triggerIndex && _prevEndIndex < triggerIndex) || (originalCollection.length && $scope.endIndex === originalCollection.length)) { |
575 | + $scope.$eval($attrs.vsScrolledToEnd); |
576 | + } |
577 | + } |
578 | + if ($attrs.vsScrolledToBeginning) { |
579 | + triggerIndex = $scope.scrolledToBeginningOffset || 0; |
580 | + if (($scope.startIndex <= triggerIndex && _prevStartIndex > $scope.startIndex)) { |
581 | + $scope.$eval($attrs.vsScrolledToBeginning); |
582 | + } |
583 | + } |
584 | + |
585 | + _prevStartIndex = $scope.startIndex; |
586 | + _prevEndIndex = $scope.endIndex; |
587 | + |
588 | + var offsetCalculationString = sizesPropertyExists ? |
589 | + '(sizesCumulative[$index + startIndex] + offsetBefore)' : |
590 | + '(($index + startIndex) * elementSize + offsetBefore)'; |
591 | + |
592 | + var parsed = $parse(offsetCalculationString); |
593 | + var o1 = parsed($scope, {$index: 0}); |
594 | + var o2 = parsed($scope, {$index: $scope[collectionName].length}); |
595 | + var total = $scope.totalSize; |
596 | + |
597 | + $beforeContent.css(getLayoutProp(), o1 + 'px'); |
598 | + $afterContent.css(getLayoutProp(), (total - o2) + 'px'); |
599 | + } |
600 | + |
601 | + return digestRequired; |
602 | + } |
603 | + } |
604 | + }; |
605 | + } |
606 | + }; |
607 | + }]); |
608 | + |
609 | + if (typeof module !== 'undefined' && module.exports) { |
610 | + module.exports = vsRepeatModule.name; |
611 | + } |
612 | +})(window, window.angular); |
613 | |
614 | === modified file 'src/maasserver/static/js/angular/maas.js' |
615 | --- src/maasserver/static/js/angular/maas.js 2017-03-10 18:38:46 +0000 |
616 | +++ src/maasserver/static/js/angular/maas.js 2017-06-15 11:16:28 +0000 |
617 | @@ -9,7 +9,8 @@ |
618 | */ |
619 | |
620 | angular.module('MAAS', |
621 | - ['ngRoute', 'ngCookies', 'ngSanitize', 'ngTagsInput', 'sticky']).config( |
622 | + ['ngRoute', 'ngCookies', 'ngSanitize', 'ngTagsInput', 'sticky', |
623 | + 'vs-repeat']).config( |
624 | function($interpolateProvider, $routeProvider, $httpProvider) { |
625 | $interpolateProvider.startSymbol('{$'); |
626 | $interpolateProvider.endSymbol('$}'); |
627 | |
628 | === modified file 'src/maasserver/static/partials/dashboard.html' |
629 | --- src/maasserver/static/partials/dashboard.html 2017-04-18 10:57:48 +0000 |
630 | +++ src/maasserver/static/partials/dashboard.html 2017-06-15 11:16:28 +0000 |
631 | @@ -46,7 +46,8 @@ |
632 | No new discoveries |
633 | </div> |
634 | </div> |
635 | - <div class="table__row" |
636 | + <div vs-repeat vs-scroll-parent="window"> |
637 | + <div class="table__row" |
638 | data-ng-repeat="discovery in discoveredDevices | orderBy:'-last_seen' track by discovery.first_seen" |
639 | data-ng-class="{'is-active' : discovery.first_seen === selectedDevice}"> |
640 | <div data-ng-if="discovery.first_seen !== selectedDevice" |
641 | @@ -179,6 +180,7 @@ |
642 | </div> |
643 | </maas-obj-form> |
644 | </div> |
645 | + </div> |
646 | </div> |
647 | </div> |
648 | </div> |
649 | |
650 | === modified file 'src/maasserver/static/partials/machines-table.html' |
651 | --- src/maasserver/static/partials/machines-table.html 2017-04-18 11:31:25 +0000 |
652 | +++ src/maasserver/static/partials/machines-table.html 2017-06-15 11:16:28 +0000 |
653 | @@ -39,7 +39,7 @@ |
654 | </th> |
655 | </tr> |
656 | </thead> |
657 | - <tbody> |
658 | + <tbody vs-repeat vs-scroll-parent="window"> |
659 | <tr |
660 | data-ng-repeat="node in filteredMachines = (machines | nodesFilter:search | orderBy:table.predicate:table.reverse) track by node.system_id" |
661 | data-ng-class="{ 'table--error': machineHasError({ $machine: node }), selected: node.$selected }"> |
662 | |
663 | === modified file 'src/maasserver/static/partials/networks-list.html' |
664 | --- src/maasserver/static/partials/networks-list.html 2017-04-10 15:35:18 +0000 |
665 | +++ src/maasserver/static/partials/networks-list.html 2017-06-15 11:16:28 +0000 |
666 | @@ -147,7 +147,7 @@ |
667 | <th class="table__column--13 align-right">Available IPs</th> |
668 | </tr> |
669 | </thead> |
670 | - <tbody> |
671 | + <tbody vs-repeat vs-scroll-parent="window"> |
672 | <tr class="table-listing__row" data-ng-repeat="row in group.spaces.rows"> |
673 | <!-- <td class="table-listing__cell table__column--3 ng-hide"> |
674 | <div data-ng-if="row.space_name"> |
675 | @@ -207,7 +207,7 @@ |
676 | <th class="table__column--25">Space</th> |
677 | </tr> |
678 | </thead> |
679 | - <tbody> |
680 | + <tbody vs-repeat vs-scroll-parent="window"> |
681 | <tr class="table-listing__row" data-ng-repeat="row in group.fabrics.rows"> |
682 | <!-- <td class="table-listing__cell table__column--3 ng-hide"> |
683 | <div data-ng-if="row.fabric_name"> |
684 | |
685 | === modified file 'src/maasserver/static/partials/node-events.html' |
686 | --- src/maasserver/static/partials/node-events.html 2017-04-05 02:36:06 +0000 |
687 | +++ src/maasserver/static/partials/node-events.html 2017-06-15 11:16:28 +0000 |
688 | @@ -35,7 +35,7 @@ |
689 | <th class="table-col--20">Time</th> |
690 | </tr> |
691 | </thead> |
692 | - <tbody> |
693 | + <tbody vs-repeat vs-scroll-parent="window"> |
694 | <tr |
695 | data-ng-repeat="event in events | filter:search | orderByDate:'created':'id' track by event.id"> |
696 | <td class="table-col--1 u-padding--right-none u-padding--left-none"> |
697 | |
698 | === modified file 'src/maasserver/static/partials/nodes-list.html' |
699 | --- src/maasserver/static/partials/nodes-list.html 2017-05-25 13:56:07 +0000 |
700 | +++ src/maasserver/static/partials/nodes-list.html 2017-06-15 11:16:28 +0000 |
701 | @@ -452,7 +452,7 @@ |
702 | <th class="table-col--5"></th> |
703 | </tr> |
704 | </thead> |
705 | - <tbody> |
706 | + <tbody vs-repeat vs-scroll-parent="window"> |
707 | <tr data-ng-repeat="interface in device.interfaces"> |
708 | <td class="table-col--20" aria-label="MAC address"> |
709 | <input type="text" id="mac-address1" placeholder="00:00:00:00:00:00" |
710 | @@ -857,7 +857,7 @@ |
711 | </th> |
712 | </tr> |
713 | </thead> |
714 | - <tbody> |
715 | + <tbody vs-repeat vs-scroll-parent="window"> |
716 | <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. --> |
717 | <tr |
718 | data-ng-repeat="device in tabs.devices.filtered_items = (devices | nodesFilter:tabs.devices.search | orderBy:tabs.devices.predicate:tabs.devices.reverse) track by device.system_id" |
719 | @@ -921,7 +921,7 @@ |
720 | </th> |
721 | </tr> |
722 | </thead> |
723 | - <tbody> |
724 | + <tbody vs-repeat vs-scroll-parent="window"> |
725 | <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. --> |
726 | <tr |
727 | data-ng-repeat="controller in tabs.controllers.filtered_items = (controllers | nodesFilter:tabs.controllers.search | orderBy:tabs.controllers.predicate:tabs.controllers.reverse) track by controller.system_id" |
728 | |
729 | === modified file 'src/maasserver/static/partials/pods-list.html' |
730 | --- src/maasserver/static/partials/pods-list.html 2017-05-01 20:31:46 +0000 |
731 | +++ src/maasserver/static/partials/pods-list.html 2017-06-15 11:16:28 +0000 |
732 | @@ -123,7 +123,7 @@ |
733 | </div> |
734 | </div> |
735 | </header> |
736 | - <div class="table__body"> |
737 | + <div class="table__body" vs-repeat vs-scroll-parent="window"> |
738 | <div class="table__row" data-ng-repeat="pod in filteredItems = (pods | nodesFilter:search | orderBy:predicate:reverse) track by pod.id" |
739 | data-ng-class="{ selected: pod.$selected, 'is-active': pod.$selected && pod.action_failed }"> |
740 | <div data-ng-if="isSuperUser()"> |
741 | |
742 | === modified file 'src/maasserver/templates/maasserver/js-conf.html' |
743 | --- src/maasserver/templates/maasserver/js-conf.html 2017-03-10 15:30:26 +0000 |
744 | +++ src/maasserver/templates/maasserver/js-conf.html 2017-06-15 11:16:28 +0000 |
745 | @@ -42,6 +42,9 @@ |
746 | src="{% url "merge" filename="sticky.js" %}?v={{files_version}}"> |
747 | </script> |
748 | <script type="text/javascript" |
749 | + src="{% url "merge" filename="vs-repeat.js" %}?v={{files_version}}"> |
750 | +</script> |
751 | +<script type="text/javascript" |
752 | src="{% url "merge" filename="maas-angular.js" %}?v={{files_version}}"> |
753 | </script> |
754 | |
755 | |
756 | === modified file 'src/maasserver/views/combo.py' |
757 | --- src/maasserver/views/combo.py 2017-05-25 13:56:07 +0000 |
758 | +++ src/maasserver/views/combo.py 2017-06-15 11:16:28 +0000 |
759 | @@ -53,6 +53,12 @@ |
760 | "js/angular/3rdparty/sticky.js", |
761 | ] |
762 | }, |
763 | + "vs-repeat.js": { |
764 | + "content_type": "text/javascript; charset=UTF-8", |
765 | + "files": [ |
766 | + "js/angular/3rdparty/vs-repeat.js", |
767 | + ] |
768 | + }, |
769 | "maas-angular.js": { |
770 | "content_type": "text/javascript; charset=UTF-8", |
771 | "files": [ |
This is awesome! Thanks for doing it.