forked from Aerolab/midnight.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
midnight.jquery.src.js
406 lines (303 loc) · 13.8 KB
/
midnight.jquery.src.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
((function ( $ ) {
$.fn.midnight = function( customOptions ) {
if( typeof customOptions !== "object" ) {
customOptions = {};
}
return this.each(function() {
// Settings
var settings = {
// The class that wraps each header. Used as a clipping mask.
headerClass: 'midnightHeader',
// The class that wraps the contents of each header. Also used as a clipping mask.
innerClass: 'midnightInner',
// The class used by the default header (useful when adding multiple headers with different markup).
defaultClass: 'default',
/*
// Add a prefix to the header classes (so if you set the "thingy-" prefix, a section with data-midnight="butterfly" will use the "thingy-butterfly" header)
classPrefix: ''
*/
};
$.extend(settings, customOptions);
// Scroll Cache
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var documentHeight = $(document).height();
// Cache all the switchable headers (different colors)
var $originalHeader = $(this);
var headers = {};
var headerInfo = {
// Todo: Add support for this (though it's mostly unnecessary)
top: 0,
height: $originalHeader.outerHeight()
};
// Sections that affect the color of the header (and cache)
var $sections = $('[data-midnight]');
var sections = [];
var getSupportedTransform = function() {
var prefixes = 'transform WebkitTransform MozTransform OTransform msTransform'.split(' ');
for(var i = 0; i < prefixes.length; i++) {
if(document.createElement('div').style[prefixes[i]] !== undefined) {
return prefixes[i];
}
}
return false;
}
var transformMode = getSupportedTransform();
// We need at least one section for this to work.
if( $sections.length == 0 ){ return; }
var getContainerHeight = function(){
var $customHeaders = $originalHeader.find('> .'+settings['headerClass']);
var maxHeight = 0;
var height = 0;
if( $customHeaders.length ) {
$customHeaders.each(function() {
var $header = $(this);
var $inner = $header.find('> .'+settings['innerClass']);
// Disable the fixed height and trigger a reflow to get the proper height
// Get the inner height or just the height of the container
if( $inner.length ) {
$inner.css('bottom', 'auto');
height = $inner.outerHeight();
$inner.css('bottom', '0');
} else {
$header.css('bottom', 'auto');
height = $header.outerHeight();
$header.css('bottom', '0');
}
maxHeight = (height > maxHeight) ? height : maxHeight;
});
} else {
maxHeight = height = $originalHeader.outerHeight();
}
return maxHeight;
};
var updateHeaderHeight = function(){
headerInfo.height = getContainerHeight();
$originalHeader.css('height', headerInfo.height+'px');
};
var setupHeaders = function(){
// Get all the different header colors
headers['default'] = {};
$sections.each(function(){
var $section = $(this);
var headerClass = $section.data('midnight');
if( typeof headerClass !== 'string' ){ return; }
headerClass = headerClass.trim();
if( headerClass === '' ){ return; }
headers[headerClass] = {};
});
// Get the padding of the original Header. It will be applied to the internal headers.
// Todo: Implement this
var defaultPaddings = {
top: $originalHeader.css("padding-top"),
right: $originalHeader.css("padding-right"),
bottom: $originalHeader.css("padding-bottom"),
left: $originalHeader.css("padding-left")
};
// Create the fake headers
$originalHeader
.css({
position: 'fixed',
top: 0,
left: 0,
right: 0,
overflow: 'hidden'
});
updateHeaderHeight();
var $customHeaders = $originalHeader.find('> .'+settings['headerClass']);
if( $customHeaders.length ) {
if( ! $customHeaders.filter('.'+ settings['defaultClass']).length ) {
// If there's no default header, just pick the first one, duplicate it, and set the correct class
$customHeaders.filter('.'+ settings['headerClass'] +':first').clone(true, true).attr('class', settings['headerClass'] +' '+ settings['defaultClass']);
}
} else {
// If there are no custom headers, just wrap the content and make that the default header
$originalHeader.wrapInner('<div class="'+ settings['headerClass'] +' '+ settings['defaultClass'] +'"></div>');
}
// Make a copy of the default header for use in the generic ones.
var $customHeaders = $originalHeader.find('> .'+ settings['headerClass']);
var $defaultHeader = $customHeaders.filter('.'+ settings['defaultClass']).clone(true, true);
for( headerClass in headers ) {
if( typeof headers[headerClass].element === 'undefined' ) {
// Create the outer clipping mask
// If there's some custom markup, use it, or else just clone the default header
var $existingHeader = $customHeaders.filter('.'+headerClass);
if( $existingHeader.length ) {
headers[headerClass].element = $existingHeader;
} else {
headers[headerClass].element = $defaultHeader.clone(true, true).removeClass( settings['defaultClass'] ).addClass(headerClass).appendTo( $originalHeader );
}
var resetStyles = {
position: 'absolute',
overflow: 'hidden',
top: 0,
left: 0,
right: 0,
bottom: 0
};
headers[headerClass].element.css(resetStyles);
if( transformMode !== false ) {
headers[headerClass].element.css(transformMode, 'translateZ(0)');
}
// Create the inner clipping mask
if( ! headers[headerClass].element.find('> .'+ settings['innerClass']).length ) {
headers[headerClass].element.wrapInner('<div class="'+ settings['innerClass'] +'"></div>');
}
headers[headerClass].inner = headers[headerClass].element.find('> .'+ settings['innerClass'])
headers[headerClass].inner.css(resetStyles);
if( transformMode !== false ) {
headers[headerClass].inner.css(transformMode, 'translateZ(0)');
}
// Set the default clipping variables
headers[headerClass].from = '';
headers[headerClass].progress = 0.0;
}
}
// Headers that weren't initialized have to be hidden
$customHeaders.each(function(){
var $header = $(this);
var hasAnyClass = false;
for( headerClass in headers ) {
if( $header.hasClass(headerClass) ){ hasAnyClass = true; }
}
// Add the inner clipping mask just in case
if( ! $header.find('> .'+ settings['innerClass']).length ) {
$header.wrapInner('<div class="'+ settings['innerClass'] +'"></div>');
}
if( ! hasAnyClass ){ $header.hide(); }
});
};
setupHeaders();
var recalculateSections = function(){
documentHeight = $(document).height();
// Cache all the sections and their start/end positions (where the class starts and ends)
sections = [];
for( i=0; i<$sections.length; i++ ) {
var $section = $($sections[i]);
sections.push({
element: $section,
class: $section.data('midnight'),
start: $section.offset().top,
end: $section.offset().top + $section.outerHeight()
});
}
};
// NANANANANANANANA GRASAAAAA
// (This is the ghetto way of keeping the section values updated after any kind of reflow. The overhead is minimal)
setInterval(recalculateSections, 1000);
var recalculateHeaders = function(){
// Check classes are currently active in the header (including the current percentage of each)
scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
// Some browsers (e.g on OS X) allow scrolling past the top/bottom.
scrollTop = Math.max(scrollTop, 0);
scrollTop = Math.min(scrollTop, documentHeight);
// Get the header's position relative to the document (given that it's fixed)
var headerHeight = headerInfo.height;
var headerStart = scrollTop + headerInfo.top;
var headerEnd = scrollTop + headerInfo.top + headerHeight;
// Reset the header status
for( ix in headers ) {
// from == '' signals that the section is inactive
headers[ ix ].from = '';
headers[ ix ].progress = 0.0;
}
// Set the header status
for( ix in sections ) {
// Todo: This isn't exactly the best code.
// If there's some kind of overlap between the header and a section, that class becomes active
if( headerEnd >= sections[ix].start && headerStart <= sections[ix].end ) {
headers[ sections[ix].class ].visible = true;
// If the header sits neatly within the section, this is the only active class
if( headerStart >= sections[ix].start && headerEnd <= sections[ix].end ) {
headers[ sections[ix].class ].from = 'top';
headers[ sections[ix].class ].progress += 1.0;
}
// If the header is in the middle of the end of a section, it comes from the top
else if( headerEnd > sections[ix].end && headerStart < sections[ix].end ) {
headers[ sections[ix].class ].from = 'top';
headers[ sections[ix].class ].progress = 1.0 - (headerEnd - sections[ix].end) / headerHeight;
}
// If the header is in the middle of the start of a section, it comes from the bottom
else if( headerEnd > sections[ix].start && headerStart < sections[ix].start ) {
// If the same color continues in the next section, just add the progress to it so we don't switch
if( headers[ sections[ix].class ].from === 'top' ) {
headers[ sections[ix].class ].progress += (headerEnd - sections[ix].start) / headerHeight;
}
else {
headers[ sections[ix].class ].from = 'bottom';
headers[ sections[ix].class ].progress = (headerEnd - sections[ix].start) / headerHeight;
}
}
}
}
};
/**
* Update the headers based on the previously calculated values
*/
var updateHeaders = function(){
// Do some preprocessing to ensure a header is always shown (even if some sections haven't been assigned)
var totalProgress = 0.0;
var lastActiveClass = '';
for( ix in headers ) {
if( ! headers[ix].from === '' ){ continue; }
totalProgress += headers[ix].progress;
lastActiveClass = ix;
}
if( totalProgress < 1.0 ) {
// Complete the header at the bottom with the default class
if( headers[ settings['defaultClass'] ].from === '' ) {
headers[ settings['defaultClass'] ].from = ( headers[lastActiveClass].from === 'top' ) ? 'bottom' : 'top';
headers[ settings['defaultClass'] ].progress = 1.0 - totalProgress;
}
else {
headers[ settings['defaultClass'] ].progress += 1.0 - totalProgress;
}
}
for( ix in headers ) {
if( ! headers[ix].from === '' ){ continue; }
var offset = (1.0 - headers[ix].progress) * 100.0;
if( headers[ix].from === 'top' ){
if( transformMode !== false ) {
headers[ix].element[0].style[transformMode] = 'translateY(-'+ offset +'%) translateZ(0)';
headers[ix].inner[0].style[transformMode] = 'translateY(+'+ offset +'%) translateZ(0)';
} else {
headers[ix].element[0].style['top'] = '-'+ offset +'%';
headers[ix].inner[0].style['top'] = '+'+ offset +'%';
}
}
else {
if( transformMode !== false ) {
headers[ix].element[0].style[transformMode] = 'translateY(+'+ offset +'%) translateZ(0)';
headers[ix].inner[0].style[transformMode] = 'translateY(-'+ offset +'%) translateZ(0)';
} else {
headers[ix].element.style['top'] = '+'+ offset +'%';
headers[ix].inner.style['top'] = '-'+ offset +'%';
}
}
}
};
// We need to recalculate all sections and headers on resize.
$(window).resize(function(){
recalculateSections();
updateHeaderHeight();
recalculateHeaders();
updateHeaders();
}).trigger('resize');
// This works using requestAnimationFrame for better compatibility with iOS/Android
requestAnimationFrame = window.requestAnimationFrame || (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
// Start the loop
var updateHeadersLoop = function(){
requestAnimationFrame(updateHeadersLoop);
recalculateHeaders();
updateHeaders();
};
updateHeadersLoop();
});
};
})(jQuery));