diff --git a/bower.json b/bower.json index 67c6184..2bd379f 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "midnight", - "version": "1.0.3", + "version": "1.1.0", "description": "Switch fixed headers on the fly", "main": "midnight.jquery.js", "homepage": "https://github.com/Aerolab/midnight.js", diff --git a/midnight.jquery.js b/midnight.jquery.js index 12a6f8a..656fd63 100644 --- a/midnight.jquery.js +++ b/midnight.jquery.js @@ -1,5 +1,5 @@ /*! - * Midnight.js 1.0.3 + * Midnight.js 1.1.0 * jQuery plugin to switch between multiple fixed header designs on the fly, so it looks in line with the content below it. * http://aerolab.github.io/midnight.js/ * @@ -8,413 +8,489 @@ * Released under the MIT license * http://aerolab.github.io/midnight.js/LICENSE.txt */ - ((function ( $ ) { + // jQuery Widget +(function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){var t=0,i=Array.prototype.slice;e.cleanData=function(t){return function(i){var s,n,a;for(a=0;null!=(n=i[a]);a++)try{s=e._data(n,"events"),s&&s.remove&&e(n).triggerHandler("remove")}catch(o){}t(i)}}(e.cleanData),e.widget=function(t,i,s){var n,a,o,r,h={},l=t.split(".")[0];return t=t.split(".")[1],n=l+"-"+t,s||(s=i,i=e.Widget),e.expr[":"][n.toLowerCase()]=function(t){return!!e.data(t,n)},e[l]=e[l]||{},a=e[l][t],o=e[l][t]=function(e,t){return this._createWidget?(arguments.length&&this._createWidget(e,t),void 0):new o(e,t)},e.extend(o,a,{version:s.version,_proto:e.extend({},s),_childConstructors:[]}),r=new i,r.options=e.widget.extend({},r.options),e.each(s,function(t,s){return e.isFunction(s)?(h[t]=function(){var e=function(){return i.prototype[t].apply(this,arguments)},n=function(e){return i.prototype[t].apply(this,e)};return function(){var t,i=this._super,a=this._superApply;return this._super=e,this._superApply=n,t=s.apply(this,arguments),this._super=i,this._superApply=a,t}}(),void 0):(h[t]=s,void 0)}),o.prototype=e.widget.extend(r,{widgetEventPrefix:a?r.widgetEventPrefix||t:t},h,{constructor:o,namespace:l,widgetName:t,widgetFullName:n}),a?(e.each(a._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete a._childConstructors):i._childConstructors.push(o),e.widget.bridge(t,o),o},e.widget.extend=function(t){for(var s,n,a=i.call(arguments,1),o=0,r=a.length;r>o;o++)for(s in a[o])n=a[o][s],a[o].hasOwnProperty(s)&&void 0!==n&&(t[s]=e.isPlainObject(n)?e.isPlainObject(t[s])?e.widget.extend({},t[s],n):e.widget.extend({},n):n);return t},e.widget.bridge=function(t,s){var n=s.prototype.widgetFullName||t;e.fn[t]=function(a){var o="string"==typeof a,r=i.call(arguments,1),h=this;return a=!o&&r.length?e.widget.extend.apply(null,[a].concat(r)):a,o?this.each(function(){var i,s=e.data(this,n);return"instance"===a?(h=s,!1):s?e.isFunction(s[a])&&"_"!==a.charAt(0)?(i=s[a].apply(s,r),i!==s&&void 0!==i?(h=i&&i.jquery?h.pushStack(i.get()):i,!1):void 0):e.error("no such method '"+a+"' for "+t+" widget instance"):e.error("cannot call methods on "+t+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var t=e.data(this,n);t?(t.option(a||{}),t._init&&t._init()):e.data(this,n,new s(a,this))}),h}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(i,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=t++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this.options=e.widget.extend({},this.options,this._getCreateOptions(),i),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(t,i){var s,n,a,o=t;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof t)if(o={},s=t.split("."),t=s.shift(),s.length){for(n=o[t]=e.widget.extend({},this.options[t]),a=0;s.length-1>a;a++)n[s[a]]=n[s[a]]||{},n=n[s[a]];if(t=s.pop(),1===arguments.length)return void 0===n[t]?null:n[t];n[t]=i}else{if(1===arguments.length)return void 0===this.options[t]?null:this.options[t];o[t]=i}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled",!!t),t&&(this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus"))),this},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_on:function(t,i,s){var n,a=this;"boolean"!=typeof t&&(s=i,i=t,t=!1),s?(i=n=e(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),e.each(s,function(s,o){function r(){return t||a.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?a[o]:o).apply(a,arguments):void 0}"string"!=typeof o&&(r.guid=o.guid=o.guid||r.guid||e.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+a.eventNamespace,u=h[2];u?n.delegate(u,l,r):i.bind(l,r)})},_off:function(t,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(i).undelegate(i),this.bindings=e(this.bindings.not(t).get()),this.focusable=e(this.focusable.not(t).get()),this.hoverable=e(this.hoverable.not(t).get())},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,o=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(o)&&o.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var o,r=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),o=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),o&&e.effects&&e.effects.effect[r]?s[t](n):r!==t&&s[r]?s[r](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}}),e.widget}); + +((function ( $ ) { "use strict"; - $.fn.midnight = function( customOptions ) { + $.widget('aerolab.midnight', { - if( typeof customOptions !== "object" ) { - customOptions = {}; - } + options: { + // 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', + // Unused: 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: '' + }, - 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', - // Unused: 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: '' - }; + // Cache all the switchable headers (different colors) + _headers: {}, + _headerInfo: {top:0, height:0}, - $.extend(settings, customOptions); + // Cache all the sections which cause the header to change colors + _$sections: [], + _sections: [], + // Scroll Cache + _scrollTop: 0, + _documentHeight: 0, - // Scroll Cache - var scrollTop = window.pageYOffset || document.documentElement.scrollTop; - var documentHeight = $(document).height(); + // Tools + _transformMode: false, - // Cache all the switchable headers (different colors) - var $originalHeader = $(this); - var headers = {}; + refresh: function() { - var headerInfo = { - // Todo: Add support for this (though it's mostly unnecessary) + this._headerInfo = { + // Todo: Add support for top (though it's mostly unnecessary) top: 0, - height: $originalHeader.outerHeight() + height: this.element.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']; - for(var ix = 0; ix < prefixes.length; ix++) { - if(document.createElement('div').style[prefixes[ix]] !== undefined) { - return prefixes[ix]; - } - } - return false; - } + this._$sections = $('[data-midnight]'); + this._sections = []; - var transformMode = getSupportedTransform(); + this._setupHeaders(); + this.recalculate(); - // We need at least one section for this to work. - if( $sections.length == 0 ){ return; } + }, + _create: function() { + + var context = this; + this._scrollTop = window.pageYOffset || document.documentElement.scrollTop; + this._documentHeight = $(document).height(); + this._headers = {}; - 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'); - } + this._transformMode = this._getSupportedTransform(); - maxHeight = (height > maxHeight) ? height : maxHeight; - }); - } else { - maxHeight = height = $originalHeader.outerHeight(); - } - return maxHeight; - }; + // Calculate all sections and create the necessary headers + this.refresh(); - var updateHeaderHeight = function(){ - headerInfo.height = getContainerHeight(); - $originalHeader.css('height', headerInfo.height+'px'); - }; + // NANANANANANANANA GRASAAAAA + // (This is the ghetto way of keeping the section values updated after any kind of reflow. The overhead is minimal) + setInterval(function(){ + context._recalculateSections(); + }, 1000); - var setupHeaders = function(){ + // We need to recalculate all this._sections and headers on resize. + $(window).resize(function(){ + context.recalculate(); + }).trigger('resize'); - // Get all the different header colors - headers['default'] = {}; - $sections.each(function(){ - var $section = $(this); - var headerClass = $section.data('midnight'); + // Start the RequestAnimationFrame loop. This should be done just once. + this._updateHeadersLoop(); - if( typeof headerClass !== 'string' ){ return; } + }, - headerClass = headerClass.trim(); - if( headerClass === '' ){ return; } + recalculate: function() { + this._recalculateSections(); + this._updateHeaderHeight(); - headers[headerClass] = {}; - }); + this._recalculateHeaders(); + this._updateHeaders(); + }, - // 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") - }; + /** + * This is to offer the optimal transform format when updating the header + */ + _getSupportedTransform: function() { + var prefixes = ['transform','WebkitTransform','MozTransform','OTransform','msTransform']; + for(var ix = 0; ix < prefixes.length; ix++) { + if(document.createElement('div').style[prefixes[ix]] !== undefined) { + return prefixes[ix]; + } + } + return false; + }, - // Create the fake headers - $originalHeader - .css({ - position: 'fixed', - top: 0, - left: 0, - right: 0, - overflow: 'hidden' - }); + /** + * Get the size of the header. + */ + _getContainerHeight: function(){ + var $customHeaders = this.element.find('> .'+this.options['headerClass']); + var maxHeight = 0; + var height = 0; + var context = this; - updateHeaderHeight(); + if( $customHeaders.length ) { + $customHeaders.each(function() { - 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']); + var $header = $(this); + var $inner = $header.find('> .'+context.options['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 ) { + // Overflow: Auto fixes an issue with Chrome 41, where outerHeight() no longer takes into account + // the margins of internal elements, creating a smaller container than necessary + $inner.css('bottom', 'auto').css('overflow', 'auto'); + height = $inner.outerHeight(); + $inner.css('bottom', '0'); + } else { + $header.css('bottom', 'auto'); + height = $header.outerHeight(); + $header.css('bottom', '0'); } - } else { - // If there are no custom headers, just wrap the content and make that the default header - $originalHeader.wrapInner('
'); - } - // 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); + maxHeight = (height > maxHeight) ? height : maxHeight; + }); + } else { + maxHeight = height = this.element.outerHeight(); + } + return maxHeight; + }, + _setupHeaders: function(){ - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( typeof headers[headerClass].element === 'undefined' ) { + // Get all the different header colors + var context = this; + this._headers[this.options['defaultClass']] = {}; - // 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 ); - } + this._$sections.each(function(){ + var $section = $(this); + var headerClass = $section.data('midnight'); - 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)'); - } + if( typeof headerClass !== 'string' ){ return; } - // Create the inner clipping mask - if( ! headers[headerClass].element.find('> .'+ settings['innerClass']).length ) { - headers[headerClass].element.wrapInner('
'); - } - headers[headerClass].inner = headers[headerClass].element.find('> .'+ settings['innerClass']) - headers[headerClass].inner.css(resetStyles); + headerClass = headerClass.trim(); - if( transformMode !== false ) { - headers[headerClass].inner.css(transformMode, 'translateZ(0)'); - } + if( headerClass === '' ){ return; } - // Set the default clipping variables - headers[headerClass].from = ''; - headers[headerClass].progress = 0.0; - } - } + context._headers[headerClass] = {}; + }); - // Headers that weren't initialized have to be hidden - $customHeaders.each(function(){ - var $header = $(this); - var hasAnyClass = false; - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( $header.hasClass(headerClass) ){ hasAnyClass = true; } - } + // Get the padding of the original Header. It will be applied to the internal headers. + // Todo: Implement this + var defaultPaddings = { + top: this.element.css("padding-top"), + right: this.element.css("padding-right"), + bottom: this.element.css("padding-bottom"), + left: this.element.css("padding-left") + }; - // Add the inner clipping mask just in case - if( ! $header.find('> .'+ settings['innerClass']).length ) { - $header.wrapInner('
'); - } - if( ! hasAnyClass ){ $header.hide(); } + // Create the fake headers + this.element + .css({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + overflow: 'hidden' }); - }; + this._updateHeaderHeight(); - setupHeaders(); + var $customHeaders = this.element.find('> .'+this.options['headerClass']); + if( $customHeaders.length ) { + if( ! $customHeaders.filter('.'+ this.options['defaultClass']).length ) { + // If there's no default header, just pick the first one, duplicate it, and set the correct class + $customHeaders.filter('.'+ this.options['headerClass'] +':first').clone(true, true).attr('class', this.options['headerClass'] +' '+ this.options['defaultClass']); + } + } else { + // If there are no custom headers, just wrap the content and make that the default header + this.element.wrapInner('
'); + } - var recalculateSections = function(){ + // Make a copy of the default header for use in the generic ones. + var $customHeaders = this.element.find('> .'+ this.options['headerClass']); + var $defaultHeader = $customHeaders.filter('.'+ this.options['defaultClass']).clone(true, true); - documentHeight = $(document).height(); - // Cache all the sections and their start/end positions (where the class starts and ends) - sections = []; - for( var ix=0; ix<$sections.length; ix++ ) { - var $section = $($sections[ix]); + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + if( typeof this._headers[headerClass].element === 'undefined' ) { - sections.push({ - element: $section, - className: $section.data('midnight'), - start: $section.offset().top, - end: $section.offset().top + $section.outerHeight() - }); - } + // 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 ) { + this._headers[headerClass].element = $existingHeader; + } else { + this._headers[headerClass].element = $defaultHeader.clone(true, true).removeClass( this.options['defaultClass'] ).addClass(headerClass).appendTo( this.element ); + } - }; + var resetStyles = { + position: 'absolute', + overflow: 'hidden', + top: 0, + left: 0, + right: 0, + bottom: 0 + }; + this._headers[headerClass].element.css(resetStyles); + if( this._transformMode !== false ) { + this._headers[headerClass].element.css(this._transformMode, 'translateZ(0)'); + } - // 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); + // Create the inner clipping mask + if( ! this._headers[headerClass].element.find('> .'+ this.options['innerClass']).length ) { + this._headers[headerClass].element.wrapInner('
'); + } + this._headers[headerClass].inner = this._headers[headerClass].element.find('> .'+ this.options['innerClass']) + this._headers[headerClass].inner.css(resetStyles); + if( this._transformMode !== false ) { + this._headers[headerClass].inner.css(this._transformMode, 'translateZ(0)'); + } - var recalculateHeaders = function(){ + // Set the default clipping variables + this._headers[headerClass].from = ''; + this._headers[headerClass].progress = 0.0; + } + } - // 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; + // Headers that weren't initialized have to be hidden + $customHeaders.each(function(){ + var $header = $(this); + var hasAnyClass = false; + for( var headerClass in context._headers ) { + if( ! context._headers.hasOwnProperty(headerClass) ){ continue; } + if( $header.hasClass(headerClass) ){ hasAnyClass = true; } + } - // Reset the header status - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - // from == '' signals that the section is inactive - headers[ headerClass ].from = ''; - headers[ headerClass ].progress = 0.0; + // Add the inner clipping mask just in case + if( ! $header.find('> .'+ context.options['innerClass']).length ) { + $header.wrapInner('
'); } - // Set the header status - for( var ix = 0; ix < sections.length; ix++ ) { + if( hasAnyClass ) { + $header.show(); + } else { + $header.hide(); + } + }); + + }, + + + /** + * Recalculate which headers should be visible at this time based on the scroll position and the (cached) position of each section. + * This doesn't update + */ + _recalculateHeaders: function(){ + + // Check classes are currently active in the header (including the current percentage of each) + this._scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; + // Some browsers (e.g on OS X) allow scrolling past the top/bottom. + this._scrollTop = Math.max(this._scrollTop, 0); + this._scrollTop = Math.min(this._scrollTop, this._documentHeight); + + // Get the header's position relative to the document (given that it's fixed) + var headerHeight = this._headerInfo.height; + var headerStart = this._scrollTop + this._headerInfo.top; + var headerEnd = headerStart + headerHeight; + + // Add support for transforms (for plugins like Headroom or general css stuff) + if( typeof window.getComputedStyle === 'function' ) { + var style = window.getComputedStyle(this.element[0], null); + var top = 0.0; + var transformY = 0.0; + + if( this._transformMode !== false && typeof style.transform === 'string' ) { + // Convert the transform matrix to an array + var transformArray = (style.transform).match(/(-?[0-9\.]+)/g); + if( transformArray !== null && transformArray.length >= 6 && ! isNaN(parseFloat(transformArray[5])) ) { + transformY = parseFloat(transformArray[5]); + } + } + if( (style.top).indexOf('px') >= 0 && ! isNaN(parseFloat(style.top)) ) { + top = parseFloat(style.top); + } + + headerStart += top + transformY; + headerEnd += top + transformY; + } + + // Reset the header status + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + // from == '' signals that the section is inactive + this._headers[ headerClass ].from = ''; + this._headers[ headerClass ].progress = 0.0; + } + + // Set the header status + for( var ix = 0; ix < this._sections.length; ix++ ) { - // Todo: This isn't exactly the best code. + // 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 ) { + // If there's some kind of overlap between the header and a section, that class becomes active + if( headerEnd >= this._sections[ix].start && headerStart <= this._sections[ix].end ) { - headers[ sections[ix].className ].visible = true; + this._headers[ this._sections[ix].className ].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].className ].from = 'top'; - headers[ sections[ix].className ].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].className ].from = 'top'; - headers[ sections[ix].className ].progress = 1.0 - (headerEnd - sections[ix].end) / headerHeight; + // If the header sits neatly within the section, this is the only active class + if( headerStart >= this._sections[ix].start && headerEnd <= this._sections[ix].end ) { + this._headers[ this._sections[ix].className ].from = 'top'; + this._headers[ this._sections[ix].className ].progress += 1.0; + } + // If the header is in the middle of the end of a section, it comes from the top + else if( headerEnd > this._sections[ix].end && headerStart < this._sections[ix].end ) { + this._headers[ this._sections[ix].className ].from = 'top'; + this._headers[ this._sections[ix].className ].progress = 1.0 - (headerEnd - this._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 > this._sections[ix].start && headerStart < this._sections[ix].start ) { + // If the same color continues in the next section, just add the progress to it so we don't switch + if( this._headers[ this._sections[ix].className ].from === 'top' ) { + this._headers[ this._sections[ix].className ].progress += (headerEnd - this._sections[ix].start) / 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].className ].from === 'top' ) { - headers[ sections[ix].className ].progress += (headerEnd - sections[ix].start) / headerHeight; - } - else { - headers[ sections[ix].className ].from = 'bottom'; - headers[ sections[ix].className ].progress = (headerEnd - sections[ix].start) / headerHeight; - } + else { + this._headers[ this._sections[ix].className ].from = 'bottom'; + this._headers[ this._sections[ix].className ].progress = (headerEnd - this._sections[ix].start) / headerHeight; } - } } - }; + } + }, - /** - * Update the headers based on the previously calculated values - */ - var updateHeaders = function(){ + /** + * Update the headers based on the position of each section + */ + _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( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( ! headers[headerClass].from === '' ){ continue; } - totalProgress += headers[headerClass].progress; - lastActiveClass = headerClass; - } + // Don't do anything if there are no headers + if( typeof this._headers[ this.options['defaultClass'] ] === 'undefined' ){ return; } + // Do some preprocessing to ensure a header is always shown (even if some this._sections haven't been assigned) + var totalProgress = 0.0; + var lastActiveClass = ''; + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + if( ! this._headers[headerClass].from === '' ){ continue; } + totalProgress += this._headers[headerClass].progress; + lastActiveClass = headerClass; + } - 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; - } + if( totalProgress < 1.0 ) { + // Complete the header at the bottom with the default class + if( this._headers[ this.options['defaultClass'] ].from === '' ) { + this._headers[ this.options['defaultClass'] ].from = ( this._headers[lastActiveClass].from === 'top' ) ? 'bottom' : 'top'; + this._headers[ this.options['defaultClass'] ].progress = 1.0 - totalProgress; } + else { + this._headers[ this.options['defaultClass'] ].progress += 1.0 - totalProgress; + } + } - for( var ix in headers ) { - if( ! headers.hasOwnProperty(ix) ){ continue; } - if( ! headers[ix].from === '' ){ continue; } + for( var ix in this._headers ) { + if( ! this._headers.hasOwnProperty(ix) ){ continue; } + if( ! this._headers[ix].from === '' ){ continue; } - var offset = (1.0 - headers[ix].progress) * 100.0; + var offset = (1.0 - this._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 +'%'; - } + // Add an extra offset when an area is hidden to prevent clipping/rounding issues. + if( offset >= 100.0 ) { offset = 110.0; } + if( offset <= -100.0 ) { offset = -110.0; } + + if( this._headers[ix].from === 'top' ){ + if( this._transformMode !== false ) { + this._headers[ix].element[0].style[this._transformMode] = 'translateY(-'+ offset +'%) translateZ(0)'; + this._headers[ix].inner[0].style[this._transformMode] = 'translateY(+'+ offset +'%) translateZ(0)'; + } else { + this._headers[ix].element[0].style['top'] = '-'+ offset +'%'; + this._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 +'%'; - } + } + else { + if( this._transformMode !== false ) { + this._headers[ix].element[0].style[this._transformMode] = 'translateY(+'+ offset +'%) translateZ(0)'; + this._headers[ix].inner[0].style[this._transformMode] = 'translateY(-'+ offset +'%) translateZ(0)'; + } else { + this._headers[ix].element[0].style['top'] = '+'+ offset +'%'; + this._headers[ix].inner[0].style['top'] = '-'+ offset +'%'; } - } - }; + } + }, - // We need to recalculate all sections and headers on resize. - $(window).resize(function(){ - recalculateSections(); - updateHeaderHeight(); + /** + * Update the size of all the sections. + * This doesn't look for new sections. It only updates the ones that were around when the plugin was started. + * Use .midnight('refresh') to do a full update. + */ + _recalculateSections: function(){ - recalculateHeaders(); - updateHeaders(); - }).trigger('resize'); + this._documentHeight = $(document).height(); + // Cache all the this._sections and their start/end positions (where the class starts and ends) + this._sections = []; + for( var ix=0; ix ."+s.headerClass),n=0,r=0;return t.length?t.each(function(){var t=e(this),a=t.find("> ."+s.innerClass);a.length?(a.css("bottom","auto"),r=a.outerHeight(),a.css("bottom","0")):(t.css("bottom","auto"),r=t.outerHeight(),t.css("bottom","0")),n=r>n?r:n}):n=r=a.outerHeight(),n},c=function(){i.height=h(),a.css("height",i.height+"px")},u=function(){o["default"]={},l.each(function(){var t=e(this),s=t.data("midnight");"string"==typeof s&&(s=s.trim(),""!==s&&(o[s]={}))});({top:a.css("padding-top"),right:a.css("padding-right"),bottom:a.css("padding-bottom"),left:a.css("padding-left")});a.css({position:"fixed",top:0,left:0,right:0,overflow:"hidden"}),c();var t=a.find("> ."+s.headerClass);t.length?t.filter("."+s.defaultClass).length||t.filter("."+s.headerClass+":first").clone(!0,!0).attr("class",s.headerClass+" "+s.defaultClass):a.wrapInner('
');var t=a.find("> ."+s.headerClass),n=t.filter("."+s.defaultClass).clone(!0,!0);for(var r in o)if(o.hasOwnProperty(r)&&"undefined"==typeof o[r].element){var i=t.filter("."+r);o[r].element=i.length?i:n.clone(!0,!0).removeClass(s.defaultClass).addClass(r).appendTo(a);var f={position:"absolute",overflow:"hidden",top:0,left:0,right:0,bottom:0};o[r].element.css(f),m!==!1&&o[r].element.css(m,"translateZ(0)"),o[r].element.find("> ."+s.innerClass).length||o[r].element.wrapInner('
'),o[r].inner=o[r].element.find("> ."+s.innerClass),o[r].inner.css(f),m!==!1&&o[r].inner.css(m,"translateZ(0)"),o[r].from="",o[r].progress=0}t.each(function(){var t=e(this),n=!1;for(var r in o)o.hasOwnProperty(r)&&t.hasClass(r)&&(n=!0);t.find("> ."+s.innerClass).length||t.wrapInner('
'),n||t.hide()})};u();var p=function(){r=e(document).height(),f=[];for(var t=0;t=f[l].start&&t<=f[l].end&&(o[f[l].className].visible=!0,t>=f[l].start&&s<=f[l].end?(o[f[l].className].from="top",o[f[l].className].progress+=1):s>f[l].end&&tf[l].start&&te&&(""===o[s.defaultClass].from?(o[s.defaultClass].from="top"===o[t].from?"bottom":"top",o[s.defaultClass].progress=1-e):o[s.defaultClass].progress+=1-e);for(var r in o)if(o.hasOwnProperty(r)&&""!==!o[r].from){var a=100*(1-o[r].progress);"top"===o[r].from?m!==!1?(o[r].element[0].style[m]="translateY(-"+a+"%) translateZ(0)",o[r].inner[0].style[m]="translateY(+"+a+"%) translateZ(0)"):(o[r].element[0].style.top="-"+a+"%",o[r].inner[0].style.top="+"+a+"%"):m!==!1?(o[r].element[0].style[m]="translateY(+"+a+"%) translateZ(0)",o[r].inner[0].style[m]="translateY(-"+a+"%) translateZ(0)"):(o[r].element.style.top="+"+a+"%",o[r].inner.style.top="-"+a+"%")}};e(window).resize(function(){p(),c(),g(),v()}).trigger("resize");var w=window.requestAnimationFrame||function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}(),C=function(){w(C),g(),v()};C()}})}}(jQuery); \ No newline at end of file +!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(t){var e=0,s=Array.prototype.slice;t.cleanData=function(e){return function(s){var i,n,o;for(o=0;null!=(n=s[o]);o++)try{i=t._data(n,"events"),i&&i.remove&&t(n).triggerHandler("remove")}catch(r){}e(s)}}(t.cleanData),t.widget=function(e,s,i){var n,o,r,a,h={},d=e.split(".")[0];return e=e.split(".")[1],n=d+"-"+e,i||(i=s,s=t.Widget),t.expr[":"][n.toLowerCase()]=function(e){return!!t.data(e,n)},t[d]=t[d]||{},o=t[d][e],r=t[d][e]=function(t,e){return this._createWidget?void(arguments.length&&this._createWidget(t,e)):new r(t,e)},t.extend(r,o,{version:i.version,_proto:t.extend({},i),_childConstructors:[]}),a=new s,a.options=t.widget.extend({},a.options),t.each(i,function(e,i){return t.isFunction(i)?void(h[e]=function(){var t=function(){return s.prototype[e].apply(this,arguments)},n=function(t){return s.prototype[e].apply(this,t)};return function(){var e,s=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=i.apply(this,arguments),this._super=s,this._superApply=o,e}}()):void(h[e]=i)}),r.prototype=t.widget.extend(a,{widgetEventPrefix:o?a.widgetEventPrefix||e:e},h,{constructor:r,namespace:d,widgetName:e,widgetFullName:n}),o?(t.each(o._childConstructors,function(e,s){var i=s.prototype;t.widget(i.namespace+"."+i.widgetName,r,s._proto)}),delete o._childConstructors):s._childConstructors.push(r),t.widget.bridge(e,r),r},t.widget.extend=function(e){for(var i,n,o=s.call(arguments,1),r=0,a=o.length;a>r;r++)for(i in o[r])n=o[r][i],o[r].hasOwnProperty(i)&&void 0!==n&&(e[i]=t.isPlainObject(n)?t.isPlainObject(e[i])?t.widget.extend({},e[i],n):t.widget.extend({},n):n);return e},t.widget.bridge=function(e,i){var n=i.prototype.widgetFullName||e;t.fn[e]=function(o){var r="string"==typeof o,a=s.call(arguments,1),h=this;return o=!r&&a.length?t.widget.extend.apply(null,[o].concat(a)):o,this.each(r?function(){var s,i=t.data(this,n);return"instance"===o?(h=i,!1):i?t.isFunction(i[o])&&"_"!==o.charAt(0)?(s=i[o].apply(i,a),s!==i&&void 0!==s?(h=s&&s.jquery?h.pushStack(s.get()):s,!1):void 0):t.error("no such method '"+o+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; attempted to call method '"+o+"'")}:function(){var e=t.data(this,n);e?(e.option(o||{}),e._init&&e._init()):t.data(this,n,new i(o,this))}),h}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(s,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=e++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),s),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(e,s){var i,n,o,r=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(r={},i=e.split("."),e=i.shift(),i.length){for(n=r[e]=t.widget.extend({},this.options[e]),o=0;i.length-1>o;o++)n[i[o]]=n[i[o]]||{},n=n[i[o]];if(e=i.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=s}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];r[e]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled",!!e),e&&(this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus"))),this},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_on:function(e,s,i){var n,o=this;"boolean"!=typeof e&&(i=s,s=e,e=!1),i?(s=n=t(s),this.bindings=this.bindings.add(s)):(i=s,s=this.element,n=this.widget()),t.each(i,function(i,r){function a(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?o[r]:r).apply(o,arguments):void 0}"string"!=typeof r&&(a.guid=r.guid=r.guid||a.guid||t.guid++);var h=i.match(/^([\w:-]*)\s*(.*)$/),d=h[1]+o.eventNamespace,l=h[2];l?n.delegate(l,d,a):s.bind(d,a)})},_off:function(e,s){s=(s||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(s).undelegate(s),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function s(){return("string"==typeof t?i[t]:t).apply(i,arguments)}var i=this;return setTimeout(s,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,s,i){var n,o,r=this.options[e];if(i=i||{},s=t.Event(s),s.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),s.target=this.element[0],o=s.originalEvent)for(n in o)n in s||(s[n]=o[n]);return this.element.trigger(s,i),!(t.isFunction(r)&&r.apply(this.element[0],[s].concat(i))===!1||s.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,s){t.Widget.prototype["_"+e]=function(i,n,o){"string"==typeof n&&(n={effect:n});var r,a=n?n===!0||"number"==typeof n?s:n.effect||s:e;n=n||{},"number"==typeof n&&(n={duration:n}),r=!t.isEmptyObject(n),n.complete=o,n.delay&&i.delay(n.delay),r&&t.effects&&t.effects.effect[a]?i[e](n):a!==e&&i[a]?i[a](n.duration,n.easing,o):i.queue(function(s){t(this)[e](),o&&o.call(i[0]),s()})}}),t.widget}),function(t){"use strict";t.widget("aerolab.midnight",{options:{headerClass:"midnightHeader",innerClass:"midnightInner",defaultClass:"default",classPrefix:""},_headers:{},_headerInfo:{top:0,height:0},_$sections:[],_sections:[],_scrollTop:0,_documentHeight:0,_transformMode:!1,refresh:function(){this._headerInfo={top:0,height:this.element.outerHeight()},this._$sections=t("[data-midnight]"),this._sections=[],this._setupHeaders(),this.recalculate()},_create:function(){var e=this;this._scrollTop=window.pageYOffset||document.documentElement.scrollTop,this._documentHeight=t(document).height(),this._headers={},this._transformMode=this._getSupportedTransform(),this.refresh(),setInterval(function(){e._recalculateSections()},1e3),t(window).resize(function(){e.recalculate()}).trigger("resize"),this._updateHeadersLoop()},recalculate:function(){this._recalculateSections(),this._updateHeaderHeight(),this._recalculateHeaders(),this._updateHeaders()},_getSupportedTransform:function(){for(var t=["transform","WebkitTransform","MozTransform","OTransform","msTransform"],e=0;e ."+this.options.headerClass),s=0,i=0,n=this;return e.length?e.each(function(){var e=t(this),o=e.find("> ."+n.options.innerClass);o.length?(o.css("bottom","auto").css("overflow","auto"),i=o.outerHeight(),o.css("bottom","0")):(e.css("bottom","auto"),i=e.outerHeight(),e.css("bottom","0")),s=i>s?i:s}):s=i=this.element.outerHeight(),s},_setupHeaders:function(){var e=this;this._headers[this.options.defaultClass]={},this._$sections.each(function(){var s=t(this),i=s.data("midnight");"string"==typeof i&&(i=i.trim(),""!==i&&(e._headers[i]={}))});({top:this.element.css("padding-top"),right:this.element.css("padding-right"),bottom:this.element.css("padding-bottom"),left:this.element.css("padding-left")});this.element.css({position:"fixed",top:0,left:0,right:0,overflow:"hidden"}),this._updateHeaderHeight();var s=this.element.find("> ."+this.options.headerClass);s.length?s.filter("."+this.options.defaultClass).length||s.filter("."+this.options.headerClass+":first").clone(!0,!0).attr("class",this.options.headerClass+" "+this.options.defaultClass):this.element.wrapInner('
');var s=this.element.find("> ."+this.options.headerClass),i=s.filter("."+this.options.defaultClass).clone(!0,!0);for(var n in this._headers)if(this._headers.hasOwnProperty(n)&&"undefined"==typeof this._headers[n].element){var o=s.filter("."+n);this._headers[n].element=o.length?o:i.clone(!0,!0).removeClass(this.options.defaultClass).addClass(n).appendTo(this.element);var r={position:"absolute",overflow:"hidden",top:0,left:0,right:0,bottom:0};this._headers[n].element.css(r),this._transformMode!==!1&&this._headers[n].element.css(this._transformMode,"translateZ(0)"),this._headers[n].element.find("> ."+this.options.innerClass).length||this._headers[n].element.wrapInner('
'),this._headers[n].inner=this._headers[n].element.find("> ."+this.options.innerClass),this._headers[n].inner.css(r),this._transformMode!==!1&&this._headers[n].inner.css(this._transformMode,"translateZ(0)"),this._headers[n].from="",this._headers[n].progress=0}s.each(function(){var s=t(this),i=!1;for(var n in e._headers)e._headers.hasOwnProperty(n)&&s.hasClass(n)&&(i=!0);s.find("> ."+e.options.innerClass).length||s.wrapInner('
'),i?s.show():s.hide()})},_recalculateHeaders:function(){this._scrollTop=window.pageYOffset||document.body.scrollTop||document.documentElement.scrollTop,this._scrollTop=Math.max(this._scrollTop,0),this._scrollTop=Math.min(this._scrollTop,this._documentHeight);var t=this._headerInfo.height,e=this._scrollTop+this._headerInfo.top,s=e+t;if("function"==typeof window.getComputedStyle){var i=window.getComputedStyle(this.element[0],null),n=0,o=0;if(this._transformMode!==!1&&"string"==typeof i.transform){var r=i.transform.match(/(-?[0-9\.]+)/g);null!==r&&r.length>=6&&!isNaN(parseFloat(r[5]))&&(o=parseFloat(r[5]))}i.top.indexOf("px")>=0&&!isNaN(parseFloat(i.top))&&(n=parseFloat(i.top)),e+=n+o,s+=n+o}for(var a in this._headers)this._headers.hasOwnProperty(a)&&(this._headers[a].from="",this._headers[a].progress=0);for(var h=0;h=this._sections[h].start&&e<=this._sections[h].end&&(this._headers[this._sections[h].className].visible=!0,e>=this._sections[h].start&&s<=this._sections[h].end?(this._headers[this._sections[h].className].from="top",this._headers[this._sections[h].className].progress+=1):s>this._sections[h].end&&ethis._sections[h].start&&et&&(""===this._headers[this.options.defaultClass].from?(this._headers[this.options.defaultClass].from="top"===this._headers[e].from?"bottom":"top",this._headers[this.options.defaultClass].progress=1-t):this._headers[this.options.defaultClass].progress+=1-t);for(var i in this._headers)if(this._headers.hasOwnProperty(i)&&""!==!this._headers[i].from){var n=100*(1-this._headers[i].progress);n>=100&&(n=110),-100>=n&&(n=-110),"top"===this._headers[i].from?this._transformMode!==!1?(this._headers[i].element[0].style[this._transformMode]="translateY(-"+n+"%) translateZ(0)",this._headers[i].inner[0].style[this._transformMode]="translateY(+"+n+"%) translateZ(0)"):(this._headers[i].element[0].style.top="-"+n+"%",this._headers[i].inner[0].style.top="+"+n+"%"):this._transformMode!==!1?(this._headers[i].element[0].style[this._transformMode]="translateY(+"+n+"%) translateZ(0)",this._headers[i].inner[0].style[this._transformMode]="translateY(-"+n+"%) translateZ(0)"):(this._headers[i].element[0].style.top="+"+n+"%",this._headers[i].inner[0].style.top="-"+n+"%")}}},_recalculateSections:function(){this._documentHeight=t(document).height(),this._sections=[];for(var e=0;eo;o++)for(s in a[o])n=a[o][s],a[o].hasOwnProperty(s)&&void 0!==n&&(t[s]=e.isPlainObject(n)?e.isPlainObject(t[s])?e.widget.extend({},t[s],n):e.widget.extend({},n):n);return t},e.widget.bridge=function(t,s){var n=s.prototype.widgetFullName||t;e.fn[t]=function(a){var o="string"==typeof a,r=i.call(arguments,1),h=this;return a=!o&&r.length?e.widget.extend.apply(null,[a].concat(r)):a,o?this.each(function(){var i,s=e.data(this,n);return"instance"===a?(h=s,!1):s?e.isFunction(s[a])&&"_"!==a.charAt(0)?(i=s[a].apply(s,r),i!==s&&void 0!==i?(h=i&&i.jquery?h.pushStack(i.get()):i,!1):void 0):e.error("no such method '"+a+"' for "+t+" widget instance"):e.error("cannot call methods on "+t+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var t=e.data(this,n);t?(t.option(a||{}),t._init&&t._init()):e.data(this,n,new s(a,this))}),h}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(i,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=t++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this.options=e.widget.extend({},this.options,this._getCreateOptions(),i),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(t,i){var s,n,a,o=t;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof t)if(o={},s=t.split("."),t=s.shift(),s.length){for(n=o[t]=e.widget.extend({},this.options[t]),a=0;s.length-1>a;a++)n[s[a]]=n[s[a]]||{},n=n[s[a]];if(t=s.pop(),1===arguments.length)return void 0===n[t]?null:n[t];n[t]=i}else{if(1===arguments.length)return void 0===this.options[t]?null:this.options[t];o[t]=i}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled",!!t),t&&(this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus"))),this},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_on:function(t,i,s){var n,a=this;"boolean"!=typeof t&&(s=i,i=t,t=!1),s?(i=n=e(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),e.each(s,function(s,o){function r(){return t||a.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?a[o]:o).apply(a,arguments):void 0}"string"!=typeof o&&(r.guid=o.guid=o.guid||r.guid||e.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+a.eventNamespace,u=h[2];u?n.delegate(u,l,r):i.bind(l,r)})},_off:function(t,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(i).undelegate(i),this.bindings=e(this.bindings.not(t).get()),this.focusable=e(this.focusable.not(t).get()),this.hoverable=e(this.hoverable.not(t).get())},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,o=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(o)&&o.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var o,r=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),o=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),o&&e.effects&&e.effects.effect[r]?s[t](n):r!==t&&s[r]?s[r](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}}),e.widget}); + ((function ( $ ) { "use strict"; - $.fn.midnight = function( customOptions ) { + $.widget('aerolab.midnight', { - if( typeof customOptions !== "object" ) { - customOptions = {}; - } + options: { + // 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', + // Unused: 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: '' + }, - 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', - // Unused: 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: '' - }; + // Cache all the switchable headers (different colors) + _headers: {}, + _headerInfo: {top:0, height:0}, - $.extend(settings, customOptions); + // Cache all the sections which cause the header to change colors + _$sections: [], + _sections: [], + // Scroll Cache + _scrollTop: 0, + _documentHeight: 0, - // Scroll Cache - var scrollTop = window.pageYOffset || document.documentElement.scrollTop; - var documentHeight = $(document).height(); + // Tools + _transformMode: false, - // Cache all the switchable headers (different colors) - var $originalHeader = $(this); - var headers = {}; + refresh: function() { - var headerInfo = { - // Todo: Add support for this (though it's mostly unnecessary) + this._headerInfo = { + // Todo: Add support for top (though it's mostly unnecessary) top: 0, - height: $originalHeader.outerHeight() + height: this.element.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']; - for(var ix = 0; ix < prefixes.length; ix++) { - if(document.createElement('div').style[prefixes[ix]] !== undefined) { - return prefixes[ix]; - } - } - return false; - } + this._$sections = $('[data-midnight]'); + this._sections = []; - var transformMode = getSupportedTransform(); + this._setupHeaders(); + this.recalculate(); - // We need at least one section for this to work. - if( $sections.length == 0 ){ return; } + }, + _create: function() { + + var context = this; + this._scrollTop = window.pageYOffset || document.documentElement.scrollTop; + this._documentHeight = $(document).height(); + this._headers = {}; - 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'); - } + this._transformMode = this._getSupportedTransform(); - maxHeight = (height > maxHeight) ? height : maxHeight; - }); - } else { - maxHeight = height = $originalHeader.outerHeight(); - } - return maxHeight; - }; + // Calculate all sections and create the necessary headers + this.refresh(); - var updateHeaderHeight = function(){ - headerInfo.height = getContainerHeight(); - $originalHeader.css('height', headerInfo.height+'px'); - }; + // NANANANANANANANA GRASAAAAA + // (This is the ghetto way of keeping the section values updated after any kind of reflow. The overhead is minimal) + setInterval(function(){ + context._recalculateSections(); + }, 1000); - var setupHeaders = function(){ + // We need to recalculate all this._sections and headers on resize. + $(window).resize(function(){ + context.recalculate(); + }).trigger('resize'); - // Get all the different header colors - headers['default'] = {}; - $sections.each(function(){ - var $section = $(this); - var headerClass = $section.data('midnight'); + // Start the RequestAnimationFrame loop. This should be done just once. + this._updateHeadersLoop(); - if( typeof headerClass !== 'string' ){ return; } + }, - headerClass = headerClass.trim(); - if( headerClass === '' ){ return; } + recalculate: function() { + this._recalculateSections(); + this._updateHeaderHeight(); - headers[headerClass] = {}; - }); + this._recalculateHeaders(); + this._updateHeaders(); + }, - // 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") - }; + /** + * This is to offer the optimal transform format when updating the header + */ + _getSupportedTransform: function() { + var prefixes = ['transform','WebkitTransform','MozTransform','OTransform','msTransform']; + for(var ix = 0; ix < prefixes.length; ix++) { + if(document.createElement('div').style[prefixes[ix]] !== undefined) { + return prefixes[ix]; + } + } + return false; + }, - // Create the fake headers - $originalHeader - .css({ - position: 'fixed', - top: 0, - left: 0, - right: 0, - overflow: 'hidden' - }); + /** + * Get the size of the header. + */ + _getContainerHeight: function(){ + var $customHeaders = this.element.find('> .'+this.options['headerClass']); + var maxHeight = 0; + var height = 0; + var context = this; - updateHeaderHeight(); + if( $customHeaders.length ) { + $customHeaders.each(function() { - 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']); + var $header = $(this); + var $inner = $header.find('> .'+context.options['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 ) { + // Overflow: Auto fixes an issue with Chrome 41, where outerHeight() no longer takes into account + // the margins of internal elements, creating a smaller container than necessary + $inner.css('bottom', 'auto').css('overflow', 'auto'); + height = $inner.outerHeight(); + $inner.css('bottom', '0'); + } else { + $header.css('bottom', 'auto'); + height = $header.outerHeight(); + $header.css('bottom', '0'); } - } else { - // If there are no custom headers, just wrap the content and make that the default header - $originalHeader.wrapInner('
'); - } - // 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); + maxHeight = (height > maxHeight) ? height : maxHeight; + }); + } else { + maxHeight = height = this.element.outerHeight(); + } + return maxHeight; + }, + _setupHeaders: function(){ - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( typeof headers[headerClass].element === 'undefined' ) { + // Get all the different header colors + var context = this; + this._headers[this.options['defaultClass']] = {}; - // 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 ); - } + this._$sections.each(function(){ + var $section = $(this); + var headerClass = $section.data('midnight'); - 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)'); - } + if( typeof headerClass !== 'string' ){ return; } - // Create the inner clipping mask - if( ! headers[headerClass].element.find('> .'+ settings['innerClass']).length ) { - headers[headerClass].element.wrapInner('
'); - } - headers[headerClass].inner = headers[headerClass].element.find('> .'+ settings['innerClass']) - headers[headerClass].inner.css(resetStyles); + headerClass = headerClass.trim(); - if( transformMode !== false ) { - headers[headerClass].inner.css(transformMode, 'translateZ(0)'); - } + if( headerClass === '' ){ return; } - // Set the default clipping variables - headers[headerClass].from = ''; - headers[headerClass].progress = 0.0; - } - } + context._headers[headerClass] = {}; + }); - // Headers that weren't initialized have to be hidden - $customHeaders.each(function(){ - var $header = $(this); - var hasAnyClass = false; - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( $header.hasClass(headerClass) ){ hasAnyClass = true; } - } + // Get the padding of the original Header. It will be applied to the internal headers. + // Todo: Implement this + var defaultPaddings = { + top: this.element.css("padding-top"), + right: this.element.css("padding-right"), + bottom: this.element.css("padding-bottom"), + left: this.element.css("padding-left") + }; - // Add the inner clipping mask just in case - if( ! $header.find('> .'+ settings['innerClass']).length ) { - $header.wrapInner('
'); - } - if( ! hasAnyClass ){ $header.hide(); } + // Create the fake headers + this.element + .css({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + overflow: 'hidden' }); - }; + this._updateHeaderHeight(); - setupHeaders(); + var $customHeaders = this.element.find('> .'+this.options['headerClass']); + if( $customHeaders.length ) { + if( ! $customHeaders.filter('.'+ this.options['defaultClass']).length ) { + // If there's no default header, just pick the first one, duplicate it, and set the correct class + $customHeaders.filter('.'+ this.options['headerClass'] +':first').clone(true, true).attr('class', this.options['headerClass'] +' '+ this.options['defaultClass']); + } + } else { + // If there are no custom headers, just wrap the content and make that the default header + this.element.wrapInner('
'); + } - var recalculateSections = function(){ + // Make a copy of the default header for use in the generic ones. + var $customHeaders = this.element.find('> .'+ this.options['headerClass']); + var $defaultHeader = $customHeaders.filter('.'+ this.options['defaultClass']).clone(true, true); - documentHeight = $(document).height(); - // Cache all the sections and their start/end positions (where the class starts and ends) - sections = []; - for( var ix=0; ix<$sections.length; ix++ ) { - var $section = $($sections[ix]); + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + if( typeof this._headers[headerClass].element === 'undefined' ) { - sections.push({ - element: $section, - className: $section.data('midnight'), - start: $section.offset().top, - end: $section.offset().top + $section.outerHeight() - }); - } + // 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 ) { + this._headers[headerClass].element = $existingHeader; + } else { + this._headers[headerClass].element = $defaultHeader.clone(true, true).removeClass( this.options['defaultClass'] ).addClass(headerClass).appendTo( this.element ); + } - }; + var resetStyles = { + position: 'absolute', + overflow: 'hidden', + top: 0, + left: 0, + right: 0, + bottom: 0 + }; + this._headers[headerClass].element.css(resetStyles); + if( this._transformMode !== false ) { + this._headers[headerClass].element.css(this._transformMode, 'translateZ(0)'); + } - // 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); + // Create the inner clipping mask + if( ! this._headers[headerClass].element.find('> .'+ this.options['innerClass']).length ) { + this._headers[headerClass].element.wrapInner('
'); + } + this._headers[headerClass].inner = this._headers[headerClass].element.find('> .'+ this.options['innerClass']) + this._headers[headerClass].inner.css(resetStyles); + if( this._transformMode !== false ) { + this._headers[headerClass].inner.css(this._transformMode, 'translateZ(0)'); + } - var recalculateHeaders = function(){ + // Set the default clipping variables + this._headers[headerClass].from = ''; + this._headers[headerClass].progress = 0.0; + } + } - // 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; + // Headers that weren't initialized have to be hidden + $customHeaders.each(function(){ + var $header = $(this); + var hasAnyClass = false; + for( var headerClass in context._headers ) { + if( ! context._headers.hasOwnProperty(headerClass) ){ continue; } + if( $header.hasClass(headerClass) ){ hasAnyClass = true; } + } - // Reset the header status - for( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - // from == '' signals that the section is inactive - headers[ headerClass ].from = ''; - headers[ headerClass ].progress = 0.0; + // Add the inner clipping mask just in case + if( ! $header.find('> .'+ context.options['innerClass']).length ) { + $header.wrapInner('
'); } - // Set the header status - for( var ix = 0; ix < sections.length; ix++ ) { + if( hasAnyClass ) { + $header.show(); + } else { + $header.hide(); + } + }); + + }, + + + /** + * Recalculate which headers should be visible at this time based on the scroll position and the (cached) position of each section. + * This doesn't update + */ + _recalculateHeaders: function(){ + + // Check classes are currently active in the header (including the current percentage of each) + this._scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; + // Some browsers (e.g on OS X) allow scrolling past the top/bottom. + this._scrollTop = Math.max(this._scrollTop, 0); + this._scrollTop = Math.min(this._scrollTop, this._documentHeight); + + // Get the header's position relative to the document (given that it's fixed) + var headerHeight = this._headerInfo.height; + var headerStart = this._scrollTop + this._headerInfo.top; + var headerEnd = headerStart + headerHeight; + + // Add support for transforms (for plugins like Headroom or general css stuff) + if( typeof window.getComputedStyle === 'function' ) { + var style = window.getComputedStyle(this.element[0], null); + var top = 0.0; + var transformY = 0.0; + + if( this._transformMode !== false && typeof style.transform === 'string' ) { + // Convert the transform matrix to an array + var transformArray = (style.transform).match(/(-?[0-9\.]+)/g); + if( transformArray !== null && transformArray.length >= 6 && ! isNaN(parseFloat(transformArray[5])) ) { + transformY = parseFloat(transformArray[5]); + } + } + if( (style.top).indexOf('px') >= 0 && ! isNaN(parseFloat(style.top)) ) { + top = parseFloat(style.top); + } + + headerStart += top + transformY; + headerEnd += top + transformY; + } + + // Reset the header status + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + // from == '' signals that the section is inactive + this._headers[ headerClass ].from = ''; + this._headers[ headerClass ].progress = 0.0; + } + + // Set the header status + for( var ix = 0; ix < this._sections.length; ix++ ) { - // Todo: This isn't exactly the best code. + // 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 ) { + // If there's some kind of overlap between the header and a section, that class becomes active + if( headerEnd >= this._sections[ix].start && headerStart <= this._sections[ix].end ) { - headers[ sections[ix].className ].visible = true; + this._headers[ this._sections[ix].className ].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].className ].from = 'top'; - headers[ sections[ix].className ].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].className ].from = 'top'; - headers[ sections[ix].className ].progress = 1.0 - (headerEnd - sections[ix].end) / headerHeight; + // If the header sits neatly within the section, this is the only active class + if( headerStart >= this._sections[ix].start && headerEnd <= this._sections[ix].end ) { + this._headers[ this._sections[ix].className ].from = 'top'; + this._headers[ this._sections[ix].className ].progress += 1.0; + } + // If the header is in the middle of the end of a section, it comes from the top + else if( headerEnd > this._sections[ix].end && headerStart < this._sections[ix].end ) { + this._headers[ this._sections[ix].className ].from = 'top'; + this._headers[ this._sections[ix].className ].progress = 1.0 - (headerEnd - this._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 > this._sections[ix].start && headerStart < this._sections[ix].start ) { + // If the same color continues in the next section, just add the progress to it so we don't switch + if( this._headers[ this._sections[ix].className ].from === 'top' ) { + this._headers[ this._sections[ix].className ].progress += (headerEnd - this._sections[ix].start) / 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].className ].from === 'top' ) { - headers[ sections[ix].className ].progress += (headerEnd - sections[ix].start) / headerHeight; - } - else { - headers[ sections[ix].className ].from = 'bottom'; - headers[ sections[ix].className ].progress = (headerEnd - sections[ix].start) / headerHeight; - } + else { + this._headers[ this._sections[ix].className ].from = 'bottom'; + this._headers[ this._sections[ix].className ].progress = (headerEnd - this._sections[ix].start) / headerHeight; } - } } - }; + } + }, - /** - * Update the headers based on the previously calculated values - */ - var updateHeaders = function(){ + /** + * Update the headers based on the position of each section + */ + _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( var headerClass in headers ) { - if( ! headers.hasOwnProperty(headerClass) ){ continue; } - if( ! headers[headerClass].from === '' ){ continue; } - totalProgress += headers[headerClass].progress; - lastActiveClass = headerClass; - } + // Don't do anything if there are no headers + if( typeof this._headers[ this.options['defaultClass'] ] === 'undefined' ){ return; } + // Do some preprocessing to ensure a header is always shown (even if some this._sections haven't been assigned) + var totalProgress = 0.0; + var lastActiveClass = ''; + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + if( ! this._headers[headerClass].from === '' ){ continue; } + totalProgress += this._headers[headerClass].progress; + lastActiveClass = headerClass; + } - 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; - } + if( totalProgress < 1.0 ) { + // Complete the header at the bottom with the default class + if( this._headers[ this.options['defaultClass'] ].from === '' ) { + this._headers[ this.options['defaultClass'] ].from = ( this._headers[lastActiveClass].from === 'top' ) ? 'bottom' : 'top'; + this._headers[ this.options['defaultClass'] ].progress = 1.0 - totalProgress; } + else { + this._headers[ this.options['defaultClass'] ].progress += 1.0 - totalProgress; + } + } - for( var ix in headers ) { - if( ! headers.hasOwnProperty(ix) ){ continue; } - if( ! headers[ix].from === '' ){ continue; } + for( var ix in this._headers ) { + if( ! this._headers.hasOwnProperty(ix) ){ continue; } + if( ! this._headers[ix].from === '' ){ continue; } - var offset = (1.0 - headers[ix].progress) * 100.0; + var offset = (1.0 - this._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 +'%'; - } + // Add an extra offset when an area is hidden to prevent clipping/rounding issues. + if( offset >= 100.0 ) { offset = 110.0; } + if( offset <= -100.0 ) { offset = -110.0; } + + if( this._headers[ix].from === 'top' ){ + if( this._transformMode !== false ) { + this._headers[ix].element[0].style[this._transformMode] = 'translateY(-'+ offset +'%) translateZ(0)'; + this._headers[ix].inner[0].style[this._transformMode] = 'translateY(+'+ offset +'%) translateZ(0)'; + } else { + this._headers[ix].element[0].style['top'] = '-'+ offset +'%'; + this._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 +'%'; - } + } + else { + if( this._transformMode !== false ) { + this._headers[ix].element[0].style[this._transformMode] = 'translateY(+'+ offset +'%) translateZ(0)'; + this._headers[ix].inner[0].style[this._transformMode] = 'translateY(-'+ offset +'%) translateZ(0)'; + } else { + this._headers[ix].element[0].style['top'] = '+'+ offset +'%'; + this._headers[ix].inner[0].style['top'] = '-'+ offset +'%'; } - } - }; + } + }, - // We need to recalculate all sections and headers on resize. - $(window).resize(function(){ - recalculateSections(); - updateHeaderHeight(); + /** + * Update the size of all the sections. + * This doesn't look for new sections. It only updates the ones that were around when the plugin was started. + * Use .midnight('refresh') to do a full update. + */ + _recalculateSections: function(){ - recalculateHeaders(); - updateHeaders(); - }).trigger('resize'); + this._documentHeight = $(document).height(); + // Cache all the this._sections and their start/end positions (where the class starts and ends) + this._sections = []; + for( var ix=0; ix