Initial commit

This commit is contained in:
Chris Ferdinandi 2014-08-15 17:12:58 -04:00
commit e2b3f924a5
17 changed files with 1218 additions and 0 deletions

10
.gitignore vendored Executable file
View File

@ -0,0 +1,10 @@
# Node
node_modules
test/results
test/coverage
## OS X
.DS_Store
._*
.Spotlight-V100
.Trashes

7
.travis.yml Executable file
View File

@ -0,0 +1,7 @@
language: node_js
node_js:
- "0.11"
- "0.10"
before_script:
- npm install -g gulp
script: gulp

122
README.md Executable file
View File

@ -0,0 +1,122 @@
# Sticky Footer [![Build Status](https://travis-ci.org/cferdinandi/sticky-footer.svg)](https://travis-ci.org/cferdinandi/sticky-footer)
Responsive sticky footers that adjust dynamically as the screen size changes.
[Download Sticky Footer](https://github.com/cferdinandi/sticky-footer/archive/master.zip) / [View the demo](http://cferdinandi.github.io/sticky-footer/).
**In This Documentation**
1. [Getting Started](#getting-started)
2. [Installing with Package Managers](#installing-with-package-managers)
3. [Options & Settings](#options-and-settings)
4. [Browser Compatibility](#browser-compatibility)
5. [How to Contribute](#how-to-contribute)
6. [License](#license)
7. [Changelog](#changelog)
## Getting Started
Compiled and production-ready code can be found in the `dist` directory. The `src` directory contains development code. Unit tests are located in the `test` directory.
### 1. Include Sticky Footer on your site.
```html
<script src="dist/js/classList.js"></script>
<script src="dist/js/bind-polyfill.js"></script>
<script src="dist/js/sticky-footer.js"></script>
```
Sticky Footer requires [classList.js](https://github.com/eligrey/classList.js) and `bind-polyfill.js`, polyfills that extend ECMAScript 5 API support to more browsers.
### 2. Add the markup to your HTML.
```html
<div data-sticky-wrap>
Body content
</div>
<div data-sticky-footer>
Footer content
</div>
```
Add the `data-sticky-wrap` attribute to a parent `<div>` that contains all of your page content. Add the `data-sticky-footer` attribute to the parent `<div>` that contains all of your footer content.
### 3. Initialize Sticky Footer.
```html
<script>
stickyFooter.init();
</script>
```
In the footer of your page, after the content, initialize Sticky Footer. And that's it, you're done. Nice work!
## Installing with Package Managers
You can install NAMEPSACE-UP with your favorite package manager.
* **NPM:** `npm install cferdinandi/sticky-footer`
* **Bower:** `bower install https://github.com/cferdinandi/sticky-footer.git`
* **Component:** `component install cferdinandi/sticky-footer`
## Options and Settings
Sticky Footer includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings.
### Global Settings
You can pass callbacks into Sticky Footer through the `init()` function:
```javascript
sticky-footer.init({
callbacks: {
before: function () {}, // Runs before the footer is stuck
after: function () {} // Runs after the footer is stuck
}
});
```
### Use Sticky Footer events in your own scripts
You can also call Sticky Footer events in your own scripts.
#### stickyFooter.destroy()
Destroy the current `stickyFooter.init()`.
```javascript
stickyFooter.destroy();
```
## Browser Compatibility
Sticky Footer works in all modern browsers, and IE 9 and above.
Sticky Footer is built with modern JavaScript APIs, and uses progressive enhancement. If the JavaScript file fails to load, or if your site is viewed on older and less capable browsers, footers will simply float up against the content like they normally would.
## How to Contribute
In lieu of a formal style guide, take care to maintain the existing coding style. Don't forget to update the version number, the changelog (in the `readme.md` file), and when applicable, the documentation.
## License
Sticky Footer is licensed under the [MIT License](http://gomakethings.com/mit/).
## Changelog
Sticky Footer uses [semantic versioning](http://semver.org/).
* v1.0.0 - August 15, 2014
* Initial release.

35
dist/js/bind-polyfill.js vendored Executable file
View File

@ -0,0 +1,35 @@
/**
* sticky-footer v1.0.0
* Responsive sticky footers, by Chris Ferdinandi.
* http://github.com/cferdinandi/sticky-footer
*
* Free to use under the MIT License.
* http://gomakethings.com/mit/
*/
/*
* Polyfill Function.prototype.bind support for otherwise ECMA Script 5 compliant browsers
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
fNOP = function () {};
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

2
dist/js/bind-polyfill.min.js vendored Executable file
View File

@ -0,0 +1,2 @@
/** sticky-footer v1.0.0, by Chris Ferdinandi | http://github.com/cferdinandi/sticky-footer | Licensed under MIT: http://gomakethings.com/mit/ */
Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var o=Array.prototype.slice.call(arguments,1),n=this;return fNOP=function(){},fBound=function(){return n.apply(this instanceof fNOP&&t?this:t,o.concat(Array.prototype.slice.call(arguments)))},fNOP.prototype=this.prototype,fBound.prototype=new fNOP,fBound});

176
dist/js/classList.js vendored Executable file
View File

@ -0,0 +1,176 @@
/**
* sticky-footer v1.0.0
* Responsive sticky footers, by Chris Ferdinandi.
* http://github.com/cferdinandi/sticky-footer
*
* Free to use under the MIT License.
* http://gomakethings.com/mit/
*/
/*
* classList.js: Cross-browser full element.classList implementation.
* 2014-01-31
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*/
/*global self, document, DOMException */
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
if ("document" in self && !("classList" in document.createElement("_"))) {
(function (view) {
"use strict";
if (!('Element' in view)) return;
var
classListProp = "classList",
protoProp = "prototype",
elemCtrProto = view.Element[protoProp],
objCtr = Object,
strTrim = String[protoProp].trim || function () {
return this.replace(/^\s+|\s+$/g, "");
},
arrIndexOf = Array[protoProp].indexOf || function (item) {
var
i = 0,
len = this.length;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
},
// Vendors: please allow content code to instantiate DOMExceptions
DOMEx = function (type, message) {
this.name = type;
this.code = DOMException[type];
this.message = message;
},
checkTokenAndGetIndex = function (classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR",
"An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR",
"String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
},
ClassList = function (elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
i = 0,
len = classes.length;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function () {
elem.setAttribute("class", this.toString());
};
},
classListProto = ClassList[protoProp] = [],
classListGetter = function () {
return new ClassList(this);
};
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function (i) {
return this[i] || null;
};
classListProto.contains = function (token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
var
tokens = arguments,
i = 0,
l = tokens.length,
token,
updated = false;
do {
token = tokens[i] + "";
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function () {
var
tokens = arguments,
i = 0,
l = tokens.length,
token,
updated = false;
do {
token = tokens[i] + "";
var index = checkTokenAndGetIndex(this, token);
if (index !== -1) {
this.splice(index, 1);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function (token, force) {
token += "";
var
result = this.contains(token),
method = result ? force !== true && "remove" : force !== false && "add";
if (method) {
this[method](token);
}
return !result;
};
classListProto.toString = function () {
return this.join(" ");
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter,
enumerable: true,
configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
if (ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(self));
}

2
dist/js/classList.min.js vendored Executable file
View File

@ -0,0 +1,2 @@
/** sticky-footer v1.0.0, by Chris Ferdinandi | http://github.com/cferdinandi/sticky-footer | Licensed under MIT: http://gomakethings.com/mit/ */
"document"in self&&!("classList"in document.createElement("_"))&&!function(t){"use strict";if("Element"in t){var e="classList",n="prototype",i=t.Element[n],r=Object,s=String[n].trim||function(){return this.replace(/^\s+|\s+$/g,"")},a=Array[n].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1},o=function(t,e){this.name=t,this.code=DOMException[t],this.message=e},u=function(t,e){if(""===e)throw new o("SYNTAX_ERR","An invalid or illegal string was specified");if(/\s/.test(e))throw new o("INVALID_CHARACTER_ERR","String contains an invalid character");return a.call(t,e)},c=function(t){for(var e=s.call(t.getAttribute("class")||""),n=e?e.split(/\s+/):[],i=0,r=n.length;r>i;i++)this.push(n[i]);this._updateClassName=function(){t.setAttribute("class",this.toString())}},l=c[n]=[],h=function(){return new c(this)};if(o[n]=Error[n],l.item=function(t){return this[t]||null},l.contains=function(t){return t+="",-1!==u(this,t)},l.add=function(){var t,e=arguments,n=0,i=e.length,r=!1;do t=e[n]+"",-1===u(this,t)&&(this.push(t),r=!0);while(++n<i);r&&this._updateClassName()},l.remove=function(){var t,e=arguments,n=0,i=e.length,r=!1;do{t=e[n]+"";var s=u(this,t);-1!==s&&(this.splice(s,1),r=!0)}while(++n<i);r&&this._updateClassName()},l.toggle=function(t,e){t+="";var n=this.contains(t),i=n?e!==!0&&"remove":e!==!1&&"add";return i&&this[i](t),!n},l.toString=function(){return this.join(" ")},r.defineProperty){var f={get:h,enumerable:!0,configurable:!0};try{r.defineProperty(i,e,f)}catch(d){-2146823252===d.number&&(f.enumerable=!1,r.defineProperty(i,e,f))}}else r[n].__defineGetter__&&i.__defineGetter__(e,h)}}(self);

167
dist/js/sticky-footer.js vendored Executable file
View File

@ -0,0 +1,167 @@
/**
* sticky-footer v1.0.0
* Responsive sticky footers, by Chris Ferdinandi.
* http://github.com/cferdinandi/sticky-footer
*
* Free to use under the MIT License.
* http://gomakethings.com/mit/
*/
(function (root, factory) {
if ( typeof define === 'function' && define.amd ) {
define('stickyFooter', factory(root));
} else if ( typeof exports === 'object' ) {
module.exports = factory(root);
} else {
root.stickyFooter = factory(root);
}
})(this, function (root) {
'use strict';
//
// Variables
//
var stickyFooter = {}; // Object for public APIs
var supports = !!document.querySelector && !!root.addEventListener; // Feature test
var settings, wrap, footer, eventTimeout;
// Default settings
var defaults = {
callbacks: {
before: function () {},
after: function () {}
}
};
//
// Methods
//
/**
* A simple forEach() implementation for Arrays, Objects and NodeLists
* @private
* @param {Array|Object|NodeList} collection Collection of items to iterate
* @param {Function} callback Callback function for each iteration
* @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
*/
var forEach = function (collection, callback, scope) {
if (Object.prototype.toString.call(collection) === '[object Object]') {
for (var prop in collection) {
if (Object.prototype.hasOwnProperty.call(collection, prop)) {
callback.call(scope, collection[prop], prop, collection);
}
}
} else {
for (var i = 0, len = collection.length; i < len; i++) {
callback.call(scope, collection[i], i, collection);
}
}
};
/**
* Merge defaults with user options
* @private
* @param {Object} defaults Default settings
* @param {Object} options User options
* @returns {Object} Merged values of defaults and options
*/
var extend = function ( defaults, options ) {
var extended = {};
forEach(defaults, function (value, prop) {
extended[prop] = defaults[prop];
});
forEach(options, function (value, prop) {
extended[prop] = options[prop];
});
return extended;
};
/**
* Get height of the viewport
* @private
* @return {Number} Height of the viewport in pixels
*/
var getViewportHeight = function () {
return Math.max( document.documentElement.clientHeight, window.innerHeight || 0 );
};
/**
* Set page wrapper height to fill viewport (minus footer height)
* @private
* @param {Element} wrap Page wrapper
* @param {Element} footer Page footer
* @param {Object} settings
*/
var setWrapHeight = function ( wrap, footer, settings ) {
settings.callbacks.before(); // Run callbacks before...
wrap.style.minHeight = ( getViewportHeight() - footer.offsetHeight ) + 'px';
settings.callbacks.after(); // Run callbacks after...
};
/**
* Destroy the current initialization.
* @public
*/
stickyFooter.destroy = function () {
if ( !settings ) return;
window.removeEventListener( 'resize', eventThrottler, false );
settings = null;
wrap = null;
footer = null;
eventTimeout = null;
};
/**
* On window scroll and resize, only run events at a rate of 15fps for better performance
* @private
* @param {Function} eventTimeout Timeout function
* @param {NodeList} wrap The content wrapper for the page
* @param {NodeList} footer The footer for the page
* @param {Object} settings
*/
var eventThrottler = function ( eventTimeout, wrap, footer, settings ) {
if ( !eventTimeout ) {
eventTimeout = setTimeout(function() {
eventTimeout = null;
setWrapHeight( wrap, footer, settings );
}, 66);
}
};
/**
* Initialize Plugin
* @public
* @param {Object} options User settings
*/
stickyFooter.init = function ( options ) {
// feature test
if ( !supports ) return;
// Destroy any existing initializations
stickyFooter.destroy();
// Selectors and variables
settings = extend( defaults, options || {} ); // Merge user options with defaults
wrap = document.querySelector( '[data-sticky-wrap]' );
footer = document.querySelector( '[data-sticky-footer]' );
// Stick footer
document.documentElement.style.height = '100%';
document.body.style.height = '100%';
setWrapHeight( wrap, footer, settings );
window.addEventListener( 'resize', eventThrottler.bind( null, eventTimeout, wrap, footer, options ), false); // Run Sticky Footer on window resize
};
//
// Public APIs
//
return stickyFooter;
});

2
dist/js/sticky-footer.min.js vendored Executable file
View File

@ -0,0 +1,2 @@
/** sticky-footer v1.0.0, by Chris Ferdinandi | http://github.com/cferdinandi/sticky-footer | Licensed under MIT: http://gomakethings.com/mit/ */
!function(e,t){"function"==typeof define&&define.amd?define("stickyFooter",t(e)):"object"==typeof exports?module.exports=t(e):e.stickyFooter=t(e)}(this,function(e){"use strict";var t,n,o,i,c={},r=!!document.querySelector&&!!e.addEventListener,l={callbacks:{before:function(){},after:function(){}}},u=function(e,t,n){if("[object Object]"===Object.prototype.toString.call(e))for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&t.call(n,e[o],o,e);else for(var i=0,c=e.length;c>i;i++)t.call(n,e[i],i,e)},f=function(e,t){var n={};return u(e,function(t,o){n[o]=e[o]}),u(t,function(e,o){n[o]=t[o]}),n},a=function(){return Math.max(document.documentElement.clientHeight,window.innerHeight||0)},d=function(e,t,n){n.callbacks.before(),e.style.minHeight=a()-t.offsetHeight+"px",n.callbacks.after()};c.destroy=function(){t&&(window.removeEventListener("resize",s,!1),t=null,n=null,o=null,i=null)};var s=function(e,t,n,o){e||(e=setTimeout(function(){e=null,d(t,n,o)},66))};return c.init=function(e){r&&(c.destroy(),t=f(l,e||{}),n=document.querySelector("[data-sticky-wrap]"),o=document.querySelector("[data-sticky-footer]"),document.documentElement.style.height="100%",document.body.style.height="100%",d(n,o,t),window.addEventListener("resize",s.bind(null,i,n,o,e),!1))},c});

131
gulpfile.js Executable file
View File

@ -0,0 +1,131 @@
var gulp = require('gulp');
var plumber = require('gulp-plumber');
var clean = require('gulp-clean');
var rename = require('gulp-rename');
var flatten = require('gulp-flatten');
var tap = require('gulp-tap');
var header = require('gulp-header');
var jshint = require('gulp-jshint');
var stylish = require('jshint-stylish');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var sass = require('gulp-ruby-sass');
var prefix = require('gulp-autoprefixer');
var minify = require('gulp-minify-css');
var karma = require('gulp-karma');
var package = require('./package.json');
var paths = {
output : 'dist/',
scripts : {
input : [ 'src/js/*' ],
output : 'dist/js/'
},
styles : {
input : 'src/sass/**/*.scss',
output : 'dist/css/'
},
static : 'src/static/**',
test : {
input : [ 'src/js/**/*.js' ],
spec : [ 'test/spec/**/*.js' ],
coverage: 'test/coverage/',
results: 'test/results/'
}
};
var banner = {
full :
'/**\n' +
' * <%= package.name %> v<%= package.version %>\n' +
' * <%= package.description %>, by <%= package.author.name %>.\n' +
' * <%= package.repository.url %>\n' +
' * \n' +
' * Free to use under the MIT License.\n' +
' * http://gomakethings.com/mit/\n' +
' */\n\n',
min :
'/**' +
' <%= package.name %> v<%= package.version %>, by Chris Ferdinandi' +
' | <%= package.repository.url %>' +
' | Licensed under MIT: http://gomakethings.com/mit/' +
' */\n'
};
gulp.task('scripts', ['clean'], function() {
return gulp.src(paths.scripts.input)
.pipe(plumber())
.pipe(flatten())
.pipe(tap(function (file, t) {
if ( file.stat.isDirectory() ) {
var name = file.relative + '.js';
return gulp.src(file.path + '/*.js')
.pipe(concat(name))
.pipe(header(banner.full, { package : package }))
.pipe(gulp.dest(paths.scripts.output))
.pipe(rename({ suffix: '.min' }))
.pipe(uglify())
.pipe(header(banner.min, { package : package }))
.pipe(gulp.dest(paths.scripts.output));
}
}))
.pipe(header(banner.full, { package : package }))
.pipe(gulp.dest(paths.scripts.output))
.pipe(rename({ suffix: '.min' }))
.pipe(uglify())
.pipe(header(banner.min, { package : package }))
.pipe(gulp.dest(paths.scripts.output));
});
gulp.task('styles', ['clean'], function() {
return gulp.src(paths.styles.input)
.pipe(plumber())
.pipe(sass({style: 'expanded', noCache: true}))
.pipe(flatten())
.pipe(prefix('last 2 version', '> 1%'))
.pipe(header(banner.full, { package : package }))
.pipe(gulp.dest(paths.styles.output))
.pipe(rename({ suffix: '.min' }))
.pipe(minify())
.pipe(header(banner.min, { package : package }))
.pipe(gulp.dest(paths.styles.output));
});
gulp.task('static', ['clean'], function() {
return gulp.src(paths.static)
.pipe(plumber())
.pipe(gulp.dest(paths.output));
});
gulp.task('lint', function () {
return gulp.src(paths.scripts.input)
.pipe(plumber())
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
gulp.task('clean', function () {
return gulp.src([
paths.output,
paths.test.coverage,
paths.test.results
], { read: false })
.pipe(plumber())
.pipe(clean());
});
gulp.task('test', function() {
return gulp.src(paths.test.input.concat(paths.test.spec))
.pipe(plumber())
.pipe(karma({ configFile: 'test/karma.conf.js' }))
.on('error', function(err) { throw err; });
});
gulp.task('default', [
'lint',
'clean',
'scripts',
'styles',
'static',
'test'
]);

61
index.html Executable file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MyPlugin</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- A basic responsive reset -->
<style>
@-webkit-viewport { width: device-width; zoom: 1.0; }
@-moz-viewport { width: device-width; zoom: 1.0; }
@-ms-viewport { width: device-width; zoom: 1.0; }
@-o-viewport { width: device-width; zoom: 1.0; }
@viewport { width: device-width; zoom: 1.0; }
html { overflow-y: auto; }
img, audio, video, canvas { max-width: 100%; }
/* Sets body width */
.container {
max-width: 40em;
width: 88%;
margin-left: auto;
margin-right: auto;
}
</style>
<!-- HTML5 Shim for IE -->
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<section class="container" data-sticky-wrap>
<h1 style="text-align: center; font-size: 3em; margin-bottom: 0;">MyPlugin</h1>
<p style="text-align: center; font-size: 1.5em; margin: 0;">description</p>
<p style="text-align: center;"><a href="https://github.com/yourname/myplugin">MyPlugin on GitHub</a></p>
<p>Some short body content.</p>
</section>
<footer class="container" data-sticky-footer>
<hr>
<p>Footer content</p>
</footer>
<!-- Javascript -->
<script src="dist/js/classList.js"></script>
<script src="dist/js/bind-polyfill.js"></script>
<script src="dist/js/sticky-footer.js"></script>
<script>
stickyFooter.init();
</script>
</body>
</html>

37
package.json Executable file
View File

@ -0,0 +1,37 @@
{
"name": "sticky-footer",
"version": "1.0.0",
"description": "Responsive sticky footers",
"author": {
"name": "Chris Ferdinandi",
"url": "http://gomakethings.com"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "http://github.com/cferdinandi/sticky-footer"
},
"devDependencies": {
"gulp": "~3.8.0",
"gulp-autoprefixer": "0.0.7",
"gulp-clean": "^0.2.4",
"gulp-concat": "~2.2.0",
"gulp-flatten": "~0.0.2",
"gulp-tap": "~0.1.1",
"gulp-header": "^1.0.2",
"gulp-jshint": "^1.6.1",
"gulp-karma": "0.0.4",
"gulp-minify-css": "~0.3.4",
"gulp-plumber": "~0.6.2",
"gulp-rename": "~1.1.0",
"gulp-ruby-sass": "~0.7.1",
"gulp-uglify": "~0.3.0",
"jshint-stylish": "^0.2.0",
"karma": "^0.12.16",
"karma-coverage": "^0.2.4",
"karma-jasmine": "~0.2.0",
"karma-phantomjs-launcher": "^0.1.4",
"karma-spec-reporter": "0.0.13",
"karma-htmlfile-reporter": "~0.1"
}
}

26
src/js/bind-polyfill.js Executable file
View File

@ -0,0 +1,26 @@
/*
* Polyfill Function.prototype.bind support for otherwise ECMA Script 5 compliant browsers
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
fNOP = function () {};
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

167
src/js/classList.js Executable file
View File

@ -0,0 +1,167 @@
/*
* classList.js: Cross-browser full element.classList implementation.
* 2014-01-31
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*/
/*global self, document, DOMException */
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
if ("document" in self && !("classList" in document.createElement("_"))) {
(function (view) {
"use strict";
if (!('Element' in view)) return;
var
classListProp = "classList",
protoProp = "prototype",
elemCtrProto = view.Element[protoProp],
objCtr = Object,
strTrim = String[protoProp].trim || function () {
return this.replace(/^\s+|\s+$/g, "");
},
arrIndexOf = Array[protoProp].indexOf || function (item) {
var
i = 0,
len = this.length;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
},
// Vendors: please allow content code to instantiate DOMExceptions
DOMEx = function (type, message) {
this.name = type;
this.code = DOMException[type];
this.message = message;
},
checkTokenAndGetIndex = function (classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR",
"An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR",
"String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
},
ClassList = function (elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
i = 0,
len = classes.length;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function () {
elem.setAttribute("class", this.toString());
};
},
classListProto = ClassList[protoProp] = [],
classListGetter = function () {
return new ClassList(this);
};
// Most DOMException implementations don't allow calling DOMException's toString()
// on non-DOMExceptions. Error's toString() is sufficient here.
DOMEx[protoProp] = Error[protoProp];
classListProto.item = function (i) {
return this[i] || null;
};
classListProto.contains = function (token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
classListProto.add = function () {
var
tokens = arguments,
i = 0,
l = tokens.length,
token,
updated = false;
do {
token = tokens[i] + "";
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function () {
var
tokens = arguments,
i = 0,
l = tokens.length,
token,
updated = false;
do {
token = tokens[i] + "";
var index = checkTokenAndGetIndex(this, token);
if (index !== -1) {
this.splice(index, 1);
updated = true;
}
}
while (++i < l);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function (token, force) {
token += "";
var
result = this.contains(token),
method = result ? force !== true && "remove" : force !== false && "add";
if (method) {
this[method](token);
}
return !result;
};
classListProto.toString = function () {
return this.join(" ");
};
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter,
enumerable: true,
configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
if (ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
}(self));
}

158
src/js/sticky-footer.js Executable file
View File

@ -0,0 +1,158 @@
(function (root, factory) {
if ( typeof define === 'function' && define.amd ) {
define('stickyFooter', factory(root));
} else if ( typeof exports === 'object' ) {
module.exports = factory(root);
} else {
root.stickyFooter = factory(root);
}
})(this, function (root) {
'use strict';
//
// Variables
//
var stickyFooter = {}; // Object for public APIs
var supports = !!document.querySelector && !!root.addEventListener; // Feature test
var settings, wrap, footer, eventTimeout;
// Default settings
var defaults = {
callbacks: {
before: function () {},
after: function () {}
}
};
//
// Methods
//
/**
* A simple forEach() implementation for Arrays, Objects and NodeLists
* @private
* @param {Array|Object|NodeList} collection Collection of items to iterate
* @param {Function} callback Callback function for each iteration
* @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
*/
var forEach = function (collection, callback, scope) {
if (Object.prototype.toString.call(collection) === '[object Object]') {
for (var prop in collection) {
if (Object.prototype.hasOwnProperty.call(collection, prop)) {
callback.call(scope, collection[prop], prop, collection);
}
}
} else {
for (var i = 0, len = collection.length; i < len; i++) {
callback.call(scope, collection[i], i, collection);
}
}
};
/**
* Merge defaults with user options
* @private
* @param {Object} defaults Default settings
* @param {Object} options User options
* @returns {Object} Merged values of defaults and options
*/
var extend = function ( defaults, options ) {
var extended = {};
forEach(defaults, function (value, prop) {
extended[prop] = defaults[prop];
});
forEach(options, function (value, prop) {
extended[prop] = options[prop];
});
return extended;
};
/**
* Get height of the viewport
* @private
* @return {Number} Height of the viewport in pixels
*/
var getViewportHeight = function () {
return Math.max( document.documentElement.clientHeight, window.innerHeight || 0 );
};
/**
* Set page wrapper height to fill viewport (minus footer height)
* @private
* @param {Element} wrap Page wrapper
* @param {Element} footer Page footer
* @param {Object} settings
*/
var setWrapHeight = function ( wrap, footer, settings ) {
settings.callbacks.before(); // Run callbacks before...
wrap.style.minHeight = ( getViewportHeight() - footer.offsetHeight ) + 'px';
settings.callbacks.after(); // Run callbacks after...
};
/**
* Destroy the current initialization.
* @public
*/
stickyFooter.destroy = function () {
if ( !settings ) return;
window.removeEventListener( 'resize', eventThrottler, false );
settings = null;
wrap = null;
footer = null;
eventTimeout = null;
};
/**
* On window scroll and resize, only run events at a rate of 15fps for better performance
* @private
* @param {Function} eventTimeout Timeout function
* @param {NodeList} wrap The content wrapper for the page
* @param {NodeList} footer The footer for the page
* @param {Object} settings
*/
var eventThrottler = function ( eventTimeout, wrap, footer, settings ) {
if ( !eventTimeout ) {
eventTimeout = setTimeout(function() {
eventTimeout = null;
setWrapHeight( wrap, footer, settings );
}, 66);
}
};
/**
* Initialize Plugin
* @public
* @param {Object} options User settings
*/
stickyFooter.init = function ( options ) {
// feature test
if ( !supports ) return;
// Destroy any existing initializations
stickyFooter.destroy();
// Selectors and variables
settings = extend( defaults, options || {} ); // Merge user options with defaults
wrap = document.querySelector( '[data-sticky-wrap]' );
footer = document.querySelector( '[data-sticky-footer]' );
// Stick footer
document.documentElement.style.height = '100%';
document.body.style.height = '100%';
setWrapHeight( wrap, footer, settings );
window.addEventListener( 'resize', eventThrottler.bind( null, eventTimeout, wrap, footer, options ), false); // Run Sticky Footer on window resize
};
//
// Public APIs
//
return stickyFooter;
});

26
test/karma.conf.js Executable file
View File

@ -0,0 +1,26 @@
module.exports = function (config) {
config.set({
basePath : '',
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['PhantomJS'],
plugins : [
'karma-spec-reporter',
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-coverage',
'karma-htmlfile-reporter'
],
reporters : ['spec', 'coverage', 'html'],
preprocessors: {
'../src/js/**/*.js': 'coverage'
},
coverageReporter: {
type : 'html',
dir : 'coverage/'
},
htmlReporter: {
outputFile: 'results/unit-tests.html'
}
});
};

89
test/spec/spec-stickyFooter.js Executable file
View File

@ -0,0 +1,89 @@
describe('My module', function () {
//
// Helper Functions
//
/**
* Inserts plugin markup into DOM
*/
var injectElem = function () {
var elem =
'<div id="jasmine-fixture">' +
'<div data-sticky-wrap>Content</div>' +
'<div data-sticky-footer>Footer</div>' +
'</div>';
document.body.innerHTML = elem;
};
/**
* Triggers an event
* @param {String} type Type of event (ex. 'click')
* @param {Element} elem The element that triggered the event
* @link http://stackoverflow.com/a/2490876
*/
var trigger = function (type, elem) {
var event; // The custom event that will be created
if (document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent(type, true, true);
} else {
event = document.createEventObject();
event.eventType = type;
}
event.eventName = type;
if (document.createEvent) {
elem.dispatchEvent(event);
} else {
elem.fireEvent("on" + event.eventType, event);
}
};
/**
* Bind polyfill for PhantomJS
* @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function () {};
var fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
//
// Init
//
describe('Should initialize plugin', function () {
beforeEach(function () {
injectElem();
stickyFooter.init();
});
it('Document should include the stickyFooter module', function () {
expect(!!stickyFooter).toBe(true);
});
});
});