diff --git a/.gitignore b/.gitignore index b0eb24c..851b9e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ build components coverage *.orig -.idea \ No newline at end of file +.idea +bower_components/ diff --git a/README.md b/README.md index f73dfbf..9d034ae 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ A jQuery plugin that switches between multiple header designs as you scroll, so **[Check out the demo](http://aerolab.github.io/midnight.js/)** (watch the logo as you scroll). +## Installation + +Midnight is available on Bower and NPM as **midnight.js**: + +``` +bower install midnight.js + +npm install midnight.js +``` + ## Quick start Create your fixed nav (or header) as you typically would. For an example, something like this (you can use whatever markup suits you) @@ -116,8 +126,44 @@ $('nav').midnight({ }); ``` + +## Running Multiple Instances with Different Breakpoints + +If you want to run multiple instances of midnight with different breakpoints, you can use the *sectionSelector* option to choose where each nav is going to be split. + +By default, the plugin will look for all the sections with the *data-midnight* attribute, which is the default, but you can change this to suit your needs. For example: + +```js +$('nav.one').midnight({ + // By default, sectionSelector is 'midnight'. It will switch only on elements that have the data-midnight attribute. + sectionSelector: 'midnight' +}); + +$('nav.two').midnight({ + // We want this nav to switch only on elements that have the data-noon attribute. + sectionSelector: 'noon' +}); +``` + + +## CDN Hosting + +To lower hosting costs & have users load midnight.js faster, you can use the [jsDelivr CDN](http://www.jsdelivr.com/#!jquery.midnight) like so: +```htm + +``` + + +### JSDelivr + +You can use jsDelivr's [version aliasing & concocting](https://www.jsdelivr.com/features) to serve the latest minor branch version along with dependancies. For example, to download the latest patch versions of midnight.js v1.0.z together along with jQuery v1.11.z: +```htm + +``` + + ## Known Issues On iOS <7 and older Android devices scrollTop isn't updated fluently, which creates a choppy effect. It can be fixed somewhat by wrapping the body in container and detecting touch events, but we're leaving that as an open issue. We'll probably disable the effect on older mobile devices due to bad performance. -You shouldn't add any sort of padding, margin or offset (top/left/right/bottom) to the nav, since it causes issues with rendering. \ No newline at end of file +You shouldn't add any sort of padding, margin or offset (top/left/right/bottom) to the nav, since it causes issues with rendering. diff --git a/bower.json b/bower.json index 67c6184..caa6e1f 100644 --- a/bower.json +++ b/bower.json @@ -1,8 +1,11 @@ { "name": "midnight", - "version": "1.0.3", "description": "Switch fixed headers on the fly", + "version": "1.1.2", "main": "midnight.jquery.js", + "dependencies": { + "jquery": "^1.11.3" + }, "homepage": "https://github.com/Aerolab/midnight.js", "license": "MIT", "authors": [ diff --git a/gulpfile.js b/gulpfile.js index 2f16434..ee640ce 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -12,20 +12,11 @@ var getCopyright = function () { return fs.readFileSync('Copyright'); }; -gulp.task('buildfromsrc', function () { - gulp.src('./midnight.jquery.src.js') - .pipe(header(getCopyright(), {version: getVersion()})) - .pipe(concat('midnight.jquery.js')) - .pipe(gulp.dest('')); -}); - -// task -gulp.task('minifyjs', function () { +gulp.task('build', function () { gulp.src('./midnight.jquery.js') - .pipe(uglify()) - .pipe(header(getCopyright(), {version: getVersion()})) + .pipe(uglify({preserveComments:'some'})) .pipe(concat('midnight.jquery.min.js')) - .pipe(gulp.dest('')); + .pipe(gulp.dest('./')); }); -gulp.task('default', ['buildfromsrc', 'minifyjs']); \ No newline at end of file +gulp.task('default', ['build']); diff --git a/index.html b/index.html index 7391f52..d6ac500 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ Midnight.js - Switch fixed headers on the fly - + @@ -16,22 +16,22 @@ - + - + - + - + @@ -221,7 +221,7 @@

Further customization

- +
Download @@ -310,4 +310,4 @@

Further customization

- \ No newline at end of file + diff --git a/midnight.jquery.js b/midnight.jquery.js index 4a717fe..d88e605 100644 --- a/midnight.jquery.js +++ b/midnight.jquery.js @@ -1,5 +1,5 @@ /*! - * Midnight.js 1.0.3 + * Midnight.js 1.1.1 * 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,409 +8,499 @@ * 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}); - $.fn.midnight = function( customOptions ) { +((function ( $ ) { - if( typeof customOptions !== "object" ) { - customOptions = {}; - } + "use strict"; - 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: '' - */ - }; + $.widget('aerolab.midnight', { + + 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: '', + // If you want to use plugin more than once or if you want a different data attribute name (so if you set the "header" in a section use data-header) + sectionSelector: 'midnight' + }, + + // 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'.split(' '); - for(var i = 0; i < prefixes.length; i++) { - if(document.createElement('div').style[prefixes[i]] !== undefined) { - return prefixes[i]; - } - } - return false; - } + this._$sections = $('[data-'+ this.options.sectionSelector +']:not(:hidden)'); + 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 getContainerHeight = function(){ - var $customHeaders = $originalHeader.find('> .'+settings['headerClass']); - var maxHeight = 0; - var height = 0; - if( $customHeaders.length ) { - $customHeaders.each(function() { + var context = this; + this._scrollTop = window.pageYOffset || document.documentElement.scrollTop; + this._documentHeight = $(document).height(); + this._headers = {}; - var $header = $(this); - var $inner = $header.find('> .'+settings['innerClass']); + this._transformMode = this._getSupportedTransform(); - // 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'); - } + // Calculate all sections and create the necessary headers + this.refresh(); - maxHeight = (height > maxHeight) ? height : maxHeight; - }); - } else { - maxHeight = height = $originalHeader.outerHeight(); - } - return maxHeight; - }; + // 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 updateHeaderHeight = function(){ - headerInfo.height = getContainerHeight(); - $originalHeader.css('height', headerInfo.height+'px'); - }; + // We need to recalculate all this._sections and headers + context.recalculate(); - var setupHeaders = function(){ - // Get all the different header colors - headers['default'] = {}; + // and at every resize + $(window).resize(function(){ + context.recalculate(); + }); - $sections.each(function(){ - var $section = $(this); - var headerClass = $section.data('midnight'); - if( typeof headerClass !== 'string' ){ return; } + // Start the RequestAnimationFrame loop. This should be done just once. + this._updateHeadersLoop(); - headerClass = headerClass.trim(); + }, - if( headerClass === '' ){ return; } - headers[headerClass] = {}; - }); + recalculate: function() { + this._recalculateSections(); + this._updateHeaderHeight(); + 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' - }); - updateHeaderHeight(); + /** + * 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; + + 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( headerClass in headers ) { - 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 ); - } + for( var i=0; i .'+ 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 === '' ){ continue; } - // 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( headerClass in headers ) { - 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( i=0; i<$sections.length; i++ ) { - var $section = $($sections[i]); + for( var headerClass in this._headers ) { + if( ! this._headers.hasOwnProperty(headerClass) ){ continue; } + if( typeof this._headers[headerClass].element === 'undefined' ) { - sections.push({ - element: $section, - class: $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( ix in headers ) { - // from == '' signals that the section is inactive - headers[ ix ].from = ''; - headers[ ix ].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( ix in sections ) { + 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 styleTop = style.top; + 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('%') >= 0 && ! isNaN(parseFloat(styleTop)) ) { + // SAFARI ISSUE https://bugs.webkit.org/show_bug.cgi?id=29084 + top = window.innerHeight * ( parseFloat(styleTop) / 100 ); + } else if( (styleTop).indexOf('px') >= 0 && ! isNaN(parseFloat(styleTop)) ) { + top = parseFloat(style.top); + } - // Todo: This isn't exactly the best code. + headerStart += top + transformY; + headerEnd += top + transformY; + } - // 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 ) { + // 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; + } - headers[ sections[ix].class ].visible = true; + // Set the header status + for( var ix = 0; ix < this._sections.length; ix++ ) { - // 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; + // 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 >= this._sections[ix].start && headerStart <= this._sections[ix].end ) { + + this._headers[ this._sections[ix].className ].visible = true; + + // 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].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; - } + 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( ix in headers ) { - if( ! headers[ix].from === '' ){ continue; } - totalProgress += headers[ix].progress; - lastActiveClass = ix; - } + // 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( ix in headers ) { + for( var ix in this._headers ) { + if( ! this._headers.hasOwnProperty(ix) ){ continue; } + if( ! this._headers[ix].from === '' ){ continue; } - if( ! headers[ix].from === '' ){ continue; } + var offset = (1.0 - this._headers[ix].progress) * 100.0; - var offset = (1.0 - headers[ix].progress) * 100.0; + // 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( 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 +'%'; - } + 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(){ + + this._documentHeight = $(document).height(); + + // Cache all the this._sections and their start/end positions (where the class starts and ends) + this._sections = []; - recalculateHeaders(); - updateHeaders(); - }).trigger('resize'); + for( var ix=0; ix ."+s.headerClass),a=0,n=0;return t.length?t.each(function(){var t=e(this),i=t.find("> ."+s.innerClass);i.length?(i.css("bottom","auto"),n=i.outerHeight(),i.css("bottom","0")):(t.css("bottom","auto"),n=t.outerHeight(),t.css("bottom","0")),a=n>a?n:a}):a=n=r.outerHeight(),a},u=function(){l.height=c(),r.css("height",l.height+"px")},g=function(){o["default"]={},d.each(function(){var t=e(this),s=t.data("midnight");"string"==typeof s&&(s=s.trim(),""!==s&&(o[s]={}))});({top:r.css("padding-top"),right:r.css("padding-right"),bottom:r.css("padding-bottom"),left:r.css("padding-left")});r.css({position:"fixed",top:0,left:0,right:0,overflow:"hidden"}),u();var t=r.find("> ."+s.headerClass);t.length?t.filter("."+s.defaultClass).length||t.filter("."+s.headerClass+":first").clone(!0,!0).attr("class",s.headerClass+" "+s.defaultClass):r.wrapInner('
');var t=r.find("> ."+s.headerClass),a=t.filter("."+s.defaultClass).clone(!0,!0);for(headerClass in o)if("undefined"==typeof o[headerClass].element){var n=t.filter("."+headerClass);o[headerClass].element=n.length?n:a.clone(!0,!0).removeClass(s.defaultClass).addClass(headerClass).appendTo(r);var i={position:"absolute",overflow:"hidden",top:0,left:0,right:0,bottom:0};o[headerClass].element.css(i),m!==!1&&o[headerClass].element.css(m,"translateZ(0)"),o[headerClass].element.find("> ."+s.innerClass).length||o[headerClass].element.wrapInner('
'),o[headerClass].inner=o[headerClass].element.find("> ."+s.innerClass),o[headerClass].inner.css(i),m!==!1&&o[headerClass].inner.css(m,"translateZ(0)"),o[headerClass].from="",o[headerClass].progress=0}t.each(function(){var t=e(this),a=!1;for(headerClass in o)t.hasClass(headerClass)&&(a=!0);t.find("> ."+s.innerClass).length||t.wrapInner('
'),a||t.hide()})};g();var p=function(){for(n=e(document).height(),f=[],i=0;i=f[ix].start&&t<=f[ix].end&&(o[f[ix].class].visible=!0,t>=f[ix].start&&s<=f[ix].end?(o[f[ix].class].from="top",o[f[ix].class].progress+=1):s>f[ix].end&&tf[ix].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(ix in o)if(""!==!o[ix].from){var a=100*(1-o[ix].progress);"top"===o[ix].from?m!==!1?(o[ix].element[0].style[m]="translateY(-"+a+"%) translateZ(0)",o[ix].inner[0].style[m]="translateY(+"+a+"%) translateZ(0)"):(o[ix].element[0].style.top="-"+a+"%",o[ix].inner[0].style.top="+"+a+"%"):m!==!1?(o[ix].element[0].style[m]="translateY(+"+a+"%) translateZ(0)",o[ix].inner[0].style[m]="translateY(-"+a+"%) translateZ(0)"):(o[ix].element.style.top="+"+a+"%",o[ix].inner.style.top="-"+a+"%")}};e(window).resize(function(){p(),u(),x(),C()}).trigger("resize"),requestAnimationFrame=window.requestAnimationFrame||function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}();var v=function(){requestAnimationFrame(v),x(),C()};v()}})}}(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,r?this.each(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+"'")}):this.each(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:"",sectionSelector:"midnight"},_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-"+this.options.sectionSelector+"]:not(:hidden)"),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),e.recalculate(),t(window).resize(function(){e.recalculate()}),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]={};for(var s=0;s ."+this.options.headerClass);o.length?o.filter("."+this.options.defaultClass).length||o.filter("."+this.options.headerClass+":first").clone(!0,!0).attr("class",this.options.headerClass+" "+this.options.defaultClass):this.element.wrapInner('
');var o=this.element.find("> ."+this.options.headerClass),r=o.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 a=o.filter("."+n);a.length?this._headers[n].element=a:this._headers[n].element=r.clone(!0,!0).removeClass(this.options.defaultClass).addClass(n).appendTo(this.element);var h={position:"absolute",overflow:"hidden",top:0,left:0,right:0,bottom:0};this._headers[n].element.css(h),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(h),this._transformMode!==!1&&this._headers[n].inner.css(this._transformMode,"translateZ(0)"),this._headers[n].from="",this._headers[n].progress=0}o.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&&e=100&&(n=110),n<=-100&&(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;e .'+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('
'); - } - - // 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('
'); - } - 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('
'); - } - - 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)); diff --git a/package.json b/package.json index dc268a3..794cad1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { - "name": "Midnight.JS", - "version": "1.0.3", + "name": "midnight.js", + "version": "1.1.2", "description": "A jQuery plugin to switch fixed headers on the fly", + "main": "midnight.jquery.min.js", + "dependencies": { + "jquery": "~1.11.3" + }, "devDependencies": { "gulp": "^3.8.8", "gulp-concat": "^2.4.1",