-
-
Notifications
You must be signed in to change notification settings - Fork 201
/
Copy pathInfiniteScrollMixin.js
201 lines (164 loc) · 5.65 KB
/
InfiniteScrollMixin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
define( [ "Ember" ], function( Ember ) {
var get = Ember.get,
set = Ember.set;
var CSSMediaRule = window.CSSMediaRule,
CSSStyleRule = window.CSSStyleRule,
reMinWidth = /^(?:\(max-width:\s*\d+px\)\s*and\s*)?\(min-width:\s*(\d+)px\)$/,
cachedMinWidths = {};
var cssMinWidthRules = [].filter.call( document.styleSheets[0].rules, function( rule ) {
return rule instanceof CSSMediaRule
&& rule.media.length === 1
&& reMinWidth.test( rule.media[0] )
&& rule.cssRules.length > 0;
});
var cachedMinHeights = [].filter.call( document.styleSheets[0].rules, function( rule ) {
return rule instanceof CSSStyleRule
&& rule.style.minHeight !== "";
}).reduce(function( cache, rule ) {
cache[ rule.selectorText ] = parseInt( rule.style.minHeight );
return cache;
}, {} );
/**
* Generate a list of all min-width media queries and their item widths for a specific selector.
* These media queries have been defined by the lesscss function .dynamic-elems-per-row()
*/
function readMinWidths( selector ) {
if ( cachedMinWidths.hasOwnProperty( selector ) ) {
return cachedMinWidths[ selector ];
}
var data = cssMinWidthRules.filter(function( rule ) {
return rule.cssRules[0].selectorText === selector;
});
if ( !data.length ) {
throw new Error( "Invalid selector" );
}
data = data.map( function( rule ) {
return {
minWidth: Math.floor( reMinWidth.exec( rule.media[0] )[1] ),
numItems: Math.floor( 100 / parseInt( rule.cssRules[0].style.width ) )
};
});
return ( cachedMinWidths[ selector ] = data );
}
function readMinHeights( selector ) {
if ( !cachedMinHeights.hasOwnProperty( selector ) ) {
throw new Error( "Invalid selector" );
}
return cachedMinHeights[ selector ];
}
function getNeededColumns( selector ) {
return readMinWidths( selector ).reduce(function( current, next ) {
return window.innerWidth < next.minWidth
? current
: next;
}).numItems;
}
function getNeededRows( selector ) {
var minHeight = readMinHeights( selector );
return 1 + Math.ceil( getItemContainer().clientHeight / minHeight );
}
function getItemContainer() {
// the route's view hasn't been inserted yet: choose the parent element
return document.querySelector( "body > .wrapper" )
|| document.body;
}
return Ember.Mixin.create({
/**
* Define the content array location.
* Can't use a binding here!!!
*/
contentPath: "controller.model",
/**
* Don't fetch infinitely.
*/
maxAutoFetches: 3,
/**
* This is actually an ugly concept, but we need to set this data at the route, so we can
* control the fetch size before querying the data. The fetch size depends on the
* window size and css media queries containing this selector. We can't move this
* calculation to the view or to the controller, because they both aren't set up at the
* time where the size is being calculated...
*/
itemSelector: "",
/**
* Calculate how many items are needed to completely fill the container
*/
calcFetchSize: function() {
var itemSel = get( this, "itemSelector" );
var offset = get( this, "offset" );
var columns = getNeededColumns( itemSel );
var rows = getNeededRows( itemSel );
var uneven = offset % columns;
var limit = ( columns * rows ) + ( uneven > 0 ? columns - uneven : 0 );
// fetch size + number of items to fill the last row after a window resize
set( this, "limit", limit );
},
beforeModel: function() {
this._super.apply( this, arguments );
// reset on route change
set( this, "offset", 0 );
this.calcFetchSize();
},
setupController: function( controller, model ) {
this._super.apply( this, arguments );
// late bindings
var binding = get( this, "_binding_offset" );
if ( !binding ) {
var contentPath = get( this, "contentPath" );
binding = Ember.Binding.from( contentPath + ".length" ).to( "offset" );
set( this, "_binding_offset", binding );
}
binding.connect( this );
var offset = get( this, "offset" );
var limit = get( this, "limit" );
set( controller, "isFetching", false );
set( controller, "hasFetchedAll", offset < limit );
set( controller, "initialFetchSize", model.length );
},
fetchContent: function() {
return this.model();
},
actions: {
"willTransition": function () {
var binding = get( this, "_binding_offset" );
if ( binding ) {
binding.disconnect( this );
}
},
"willFetchContent": function( force ) {
var controller = get( this, "controller" );
var isFetching = get( controller, "isFetching" );
var fetchedAll = get( controller, "hasFetchedAll" );
// we're already busy or finished fetching
if ( isFetching || fetchedAll ) { return; }
this.calcFetchSize();
var content = get( this, get( this, "contentPath" ) );
var offset = get( this, "offset" );
var limit = get( this, "limit" );
var max = get( this, "maxAutoFetches" );
var num = offset / limit;
// don't fetch infinitely
if ( !force && num > max ) { return; }
set( controller, "fetchError", false );
set( controller, "isFetching", true );
// fetch content
this.fetchContent()
.then(function( data ) {
if ( !data || !data.length || data.length < limit ) {
set( controller, "hasFetchedAll", true );
}
if ( data && data.length ) {
content.pushObjects( data );
}
set( controller, "isFetching", false );
})
.catch(function( err ) {
set( controller, "fetchError", true );
set( controller, "isFetching", false );
set( controller, "hasFetchedAll", false );
return Promise.reject( err );
});
}
}
});
});