formstone
Version:
Library of modular front end components.
879 lines (677 loc) • 25.4 kB
JavaScript
/**
* @plugin
* @name Core
* @description Formstone Library core. Required for all plugins.
*/
/* global define */
/* global ga */
(function(factory) {
if (typeof define === "function" && define.amd) {
define(["jquery"], factory);
} else {
factory(jQuery);
}
}(function($) {
"use strict";
// Namespace
var Win = typeof window !== "undefined" ? window : this,
Doc = Win.document,
Core = function() {
this.Version = '@version';
this.Plugins = {};
this.DontConflict = false;
this.Conflicts = {
fn: {}
};
this.ResizeHandlers = [];
this.RAFHandlers = [];
// Globals
this.window = Win;
this.$window = $(Win);
this.document = Doc;
this.$document = $(Doc);
this.$body = null;
this.windowWidth = 0;
this.windowHeight = 0;
this.fallbackWidth = 1024;
this.fallbackHeight = 768;
this.userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera;
this.isFirefox = /Firefox/i.test(this.userAgent);
this.isChrome = /Chrome/i.test(this.userAgent);
this.isSafari = /Safari/i.test(this.userAgent) && !this.isChrome;
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(this.userAgent);
this.isIEMobile = /IEMobile/i.test(this.userAgent);
this.isFirefoxMobile = (this.isFirefox && this.isMobile);
this.transform = null;
this.transition = null;
this.support = {
file: !!(window.File && window.FileList && window.FileReader),
history: !!(window.history && window.history.pushState && window.history.replaceState),
matchMedia: !!(window.matchMedia || window.msMatchMedia),
pointer: !!(window.PointerEvent),
raf: !!(window.requestAnimationFrame && window.cancelAnimationFrame),
touch: !!(("ontouchstart" in window) || window.DocumentTouch && document instanceof window.DocumentTouch),
transition: false,
transform: false
};
},
Functions = {
/**
* @method private
* @name killEvent
* @description Stops event action and bubble.
* @param e [object] "Event data"
*/
killEvent: function(e, immediate) {
try {
e.preventDefault();
e.stopPropagation();
if (immediate) {
e.stopImmediatePropagation();
}
} catch (error) {
//
}
},
/**
* @method private
* @name killGesture
* @description Stops gesture event action.
* @param e [object] "Event data"
*/
killGesture: function(e) {
try {
e.preventDefault();
} catch (error) {
//
}
},
/**
* @method private
* @name lockViewport
* @description Unlocks the viewport, preventing getsures.
*/
lockViewport: function(plugin_namespace) {
ViewportLocks[plugin_namespace] = true;
if (!$.isEmptyObject(ViewportLocks) && !ViewportLocked) {
if ($ViewportMeta.length) {
$ViewportMeta.attr("content", ViewportMetaLocked);
} else {
$ViewportMeta = $("head").append('<meta name="viewport" content="' + ViewportMetaLocked + '">');
}
Formstone.$body.on(Events.gestureChange, Functions.killGesture)
.on(Events.gestureStart, Functions.killGesture)
.on(Events.gestureEnd, Functions.killGesture);
ViewportLocked = true;
}
},
/**
* @method private
* @name unlockViewport
* @description Unlocks the viewport, allowing getsures.
*/
unlockViewport: function(plugin_namespace) {
if ($.type(ViewportLocks[plugin_namespace]) !== 'undefined') {
delete ViewportLocks[plugin_namespace];
}
if ($.isEmptyObject(ViewportLocks) && ViewportLocked) {
if ($ViewportMeta.length) {
if (ViewportMetaOriginal) {
$ViewportMeta.attr("content", ViewportMetaOriginal);
} else {
$ViewportMeta.remove();
}
}
Formstone.$body.off(Events.gestureChange)
.off(Events.gestureStart)
.off(Events.gestureEnd);
ViewportLocked = false;
}
},
/**
* @method private
* @name startTimer
* @description Starts an internal timer.
* @param timer [int] "Timer ID"
* @param time [int] "Time until execution"
* @param callback [function] "Function to execute"
* @return [int] "Timer ID"
*/
startTimer: function(timer, time, callback, interval) {
Functions.clearTimer(timer);
return (interval) ? setInterval(callback, time) : setTimeout(callback, time);
},
/**
* @method private
* @name clearTimer
* @description Clears an internal timer.
* @param timer [int] "Timer ID"
*/
clearTimer: function(timer, interval) {
if (timer) {
if (interval) {
clearInterval(timer);
} else {
clearTimeout(timer);
}
timer = null;
}
},
/**
* @method private
* @name sortAsc
* @description Sorts an array (ascending).
* @param a [mixed] "First value"
* @param b [mixed] "Second value"
* @return Difference between second and first values
*/
sortAsc: function(a, b) {
return (parseInt(a, 10) - parseInt(b, 10));
},
/**
* @method private
* @name sortDesc
* @description Sorts an array (descending).
* @param a [mixed] "First value"
* @param b [mixed] "Second value"
* @return Difference between second and first values
*/
sortDesc: function(a, b) {
return (parseInt(b, 10) - parseInt(a, 10));
},
/**
* @method private
* @name decodeEntities
* @description Decodes HTML.
* @param string [string] "String to decode"
* @return Decoded string
*/
decodeEntities: function(string) {
// http://stackoverflow.com/a/1395954
var el = Formstone.document.createElement("textarea");
el.innerHTML = string;
return el.value;
},
/**
* @method private
* @name parseGetParams
* @description Returns keyed object containing all GET query parameters
* @param url [string] "URL to parse"
* @return [object] "Keyed query params"
*/
parseQueryString: function(url) {
var params = {},
parts = url.slice(url.indexOf("?") + 1).split("&");
for (var i = 0; i < parts.length; i++) {
var part = parts[i].split("=");
params[part[0]] = part[1];
}
return params;
}
},
Formstone = new Core(),
// Deferred ready
$Ready = $.Deferred(),
// Classes
Classes = {
base: "{ns}",
element: "{ns}-element"
},
// Events
Events = {
namespace: ".{ns}",
beforeUnload: "beforeunload.{ns}",
blur: "blur.{ns}",
change: "change.{ns}",
click: "click.{ns}",
dblClick: "dblclick.{ns}",
drag: "drag.{ns}",
dragEnd: "dragend.{ns}",
dragEnter: "dragenter.{ns}",
dragLeave: "dragleave.{ns}",
dragOver: "dragover.{ns}",
dragStart: "dragstart.{ns}",
drop: "drop.{ns}",
error: "error.{ns}",
focus: "focus.{ns}",
focusIn: "focusin.{ns}",
focusOut: "focusout.{ns}",
gestureChange: "gesturechange.{ns}",
gestureStart: "gesturestart.{ns}",
gestureEnd: "gestureend.{ns}",
input: "input.{ns}",
keyDown: "keydown.{ns}",
keyPress: "keypress.{ns}",
keyUp: "keyup.{ns}",
load: "load.{ns}",
mouseDown: "mousedown.{ns}",
mouseEnter: "mouseenter.{ns}",
mouseLeave: "mouseleave.{ns}",
mouseMove: "mousemove.{ns}",
mouseOut: "mouseout.{ns}",
mouseOver: "mouseover.{ns}",
mouseUp: "mouseup.{ns}",
panStart: "panstart.{ns}",
pan: "pan.{ns}",
panEnd: "panend.{ns}",
resize: "resize.{ns}",
scaleStart: "scalestart.{ns}",
scaleEnd: "scaleend.{ns}",
scale: "scale.{ns}",
scroll: "scroll.{ns}",
select: "select.{ns}",
swipe: "swipe.{ns}",
touchCancel: "touchcancel.{ns}",
touchEnd: "touchend.{ns}",
touchLeave: "touchleave.{ns}",
touchMove: "touchmove.{ns}",
touchStart: "touchstart.{ns}"
},
ResizeTimer = null,
Debounce = 20,
$ViewportMeta,
ViewportMetaOriginal,
ViewportMetaLocked,
ViewportLocks = [],
ViewportLocked = false;
/**
* @method
* @name NoConflict
* @description Resolves plugin namespace conflicts
* @example Formstone.NoConflict();
*/
Core.prototype.NoConflict = function() {
Formstone.DontConflict = true;
for (var i in Formstone.Plugins) {
if (Formstone.Plugins.hasOwnProperty(i)) {
$[i] = Formstone.Conflicts[i];
$.fn[i] = Formstone.Conflicts.fn[i];
}
}
};
/**
* @method
* @name Ready
* @description Replacement for jQuery ready
* @param e [object] "Event data"
*/
Core.prototype.Ready = function(fn) {
if (
Formstone.document.readyState === "complete" ||
(Formstone.document.readyState !== "loading" && !Formstone.document.documentElement.doScroll)
) {
fn();
} else {
Formstone.document.addEventListener("DOMContentLoaded", fn);
}
};
/**
* @method
* @name Plugin
* @description Builds a plugin and registers it with jQuery.
* @param namespace [string] "Plugin namespace"
* @param settings [object] "Plugin settings"
* @return [object] "Plugin properties. Includes `defaults`, `classes`, `events`, `functions`, `methods` and `utilities` keys"
* @example Formstone.Plugin("namespace", { ... });
*/
Core.prototype.Plugin = function(namespace, settings) {
Formstone.Plugins[namespace] = (function(namespace, settings) {
var namespaceDash = "fs-" + namespace,
namespaceDot = "fs." + namespace,
namespaceClean = "fs" + namespace.replace(/(^|\s)([a-z])/g, function(m, p1, p2) {
return p1 + p2.toUpperCase();
});
/**
* @method private
* @name initialize
* @description Creates plugin instance by adding base classname, creating data and scoping a _construct call.
* @param options [object] <{}> "Instance options"
*/
function initialize(options) {
// Maintain Chain
var hasOptions = $.type(options) === "object",
args = Array.prototype.slice.call(arguments, (hasOptions ? 1 : 0)),
$targets = this,
$postTargets = $(),
$element,
i,
count;
// Extend Defaults
options = $.extend(true, {}, settings.defaults || {}, (hasOptions ? options : {}));
// All targets
for (i = 0, count = $targets.length; i < count; i++) {
$element = $targets.eq(i);
// Gaurd Against Exiting Instances
if (!getData($element)) {
// Extend w/ Local Options
settings.guid++;
var guid = "__" + settings.guid,
rawGuid = settings.classes.raw.base + guid,
locals = $element.data(namespace + "-options"),
data = $.extend(true, {
$el: $element,
guid: guid,
numGuid: settings.guid,
rawGuid: rawGuid,
dotGuid: "." + rawGuid
}, options, ($.type(locals) === "object" ? locals : {}));
// Cache Instance
$element.addClass(settings.classes.raw.element)
.data(namespaceClean, data);
// Constructor
settings.methods._construct.apply($element, [data].concat(args));
// Post Constructor
$postTargets = $postTargets.add($element);
}
}
// Post targets
for (i = 0, count = $postTargets.length; i < count; i++) {
$element = $postTargets.eq(i);
// Post Constructor
settings.methods._postConstruct.apply($element, [getData($element)]);
}
return $targets;
}
/**
* @method private
* @name destroy
* @description Removes plugin instance by scoping a _destruct call, and removing the base classname and data.
* @param data [object] <{}> "Instance data"
*/
/**
* @method widget
* @name destroy
* @description Removes plugin instance.
* @example $(".target").{ns}("destroy");
*/
function destroy(data) {
settings.functions.iterate.apply(this, [settings.methods._destruct].concat(Array.prototype.slice.call(arguments, 1)));
this.removeClass(settings.classes.raw.element)
.removeData(namespaceClean);
}
/**
* @method private
* @name getData
* @description Creates class selector from text.
* @param $element [jQuery] "Target jQuery object"
* @return [object] "Instance data"
*/
function getData($el) {
return $el.data(namespaceClean);
}
/**
* @method private
* @name delegateWidget
* @description Delegates public methods.
* @param method [string] "Method to execute"
* @return [jQuery] "jQuery object"
*/
function delegateWidget(method) {
// If jQuery object
if (this instanceof $) {
var _method = settings.methods[method];
// Public method OR false
if ($.type(method) === "object" || !method) {
// Initialize
return initialize.apply(this, arguments);
} else if (_method && method.indexOf("_") !== 0) {
// Wrap Public Methods
var args = [_method].concat(Array.prototype.slice.call(arguments, 1));
return settings.functions.iterate.apply(this, args);
}
return this;
}
}
/**
* @method private
* @name delegateUtility
* @description Delegates utility methods.
* @param method [string] "Method to execute"
*/
function delegateUtility(method) {
// public utility OR utility init OR false
var _method = settings.utilities[method] || settings.utilities._initialize || false;
if (_method) {
// Wrap Utility Methods
var args = Array.prototype.slice.call(arguments, ($.type(method) === "object" ? 0 : 1));
return _method.apply(window, args);
}
}
/**
* @method utility
* @name defaults
* @description Extends plugin default settings; effects instances created hereafter.
* @param options [object] <{}> "New plugin defaults"
* @example $.{ns}("defaults", { ... });
*/
function defaults(options) {
settings.defaults = $.extend(true, settings.defaults, options || {});
}
/**
* @method private
* @name iterate
* @description Loops scoped function calls over jQuery object with instance data as first parameter.
* @param func [function] "Function to execute"
* @return [jQuery] "jQuery object"
*/
function iterate(fn) {
var $targets = this,
args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, count = $targets.length; i < count; i++) {
var $element = $targets.eq(i),
data = getData($element) || {};
if ($.type(data.$el) !== "undefined") {
fn.apply($element, [data].concat(args));
}
}
return $targets;
}
// Locals
settings.initialized = false;
settings.priority = settings.priority || 10;
// Namespace Classes & Events
settings.classes = namespaceProperties("classes", namespaceDash, Classes, settings.classes);
settings.events = namespaceProperties("events", namespace, Events, settings.events);
// Extend Functions
settings.functions = $.extend({
getData: getData,
iterate: iterate
}, Functions, settings.functions);
// Extend Methods
settings.methods = $.extend(true, {
// Private Methods
_construct: $.noop, // Constructor
_postConstruct: $.noop, // Post Constructor
_destruct: $.noop, // Destructor
_resize: false, // Window resize
// Public Methods
destroy: destroy
}, settings.methods);
// Extend Utilities
settings.utilities = $.extend(true, {
// Private Utilities
_initialize: false, // First Run
_delegate: false, // Custom Delegation
// Public Utilities
defaults: defaults
}, settings.utilities);
// Register Plugin
// Widget
if (settings.widget) {
// Store conflicting namesapaces
Formstone.Conflicts.fn[namespace] = $.fn[namespace];
// Widget Delegation: $(".target").fsPlugin("method", ...);
$.fn[namespaceClean] = delegateWidget;
if (!Formstone.DontConflict) {
// $(".target").plugin("method", ...);
$.fn[namespace] = $.fn[namespaceClean];
}
}
// Utility
Formstone.Conflicts[namespace] = $[namespace];
// Utility Delegation: $.fsPlugin("method", ... );
$[namespaceClean] = settings.utilities._delegate || delegateUtility;
if (!Formstone.DontConflict) {
// $.plugin("method", ... );
$[namespace] = $[namespaceClean];
}
// Run Setup
settings.namespace = namespace;
settings.namespaceClean = namespaceClean;
settings.guid = 0;
// Resize handler
if (settings.methods._resize) {
Formstone.ResizeHandlers.push({
namespace: namespace,
priority: settings.priority,
callback: settings.methods._resize
});
// Sort handlers on push
Formstone.ResizeHandlers.sort(sortPriority);
}
// RAF handler
if (settings.methods._raf) {
Formstone.RAFHandlers.push({
namespace: namespace,
priority: settings.priority,
callback: settings.methods._raf
});
// Sort handlers on push
Formstone.RAFHandlers.sort(sortPriority);
}
return settings;
})(namespace, settings);
return Formstone.Plugins[namespace];
};
// Namespace Properties
function namespaceProperties(type, namespace, globalProps, customProps) {
var _props = {
raw: {}
},
i;
customProps = customProps || {};
for (i in customProps) {
if (customProps.hasOwnProperty(i)) {
if (type === "classes") {
// Custom classes
_props.raw[customProps[i]] = namespace + "-" + customProps[i];
_props[customProps[i]] = "." + namespace + "-" + customProps[i];
} else {
// Custom events
_props.raw[i] = customProps[i];
_props[i] = customProps[i] + "." + namespace;
}
}
}
for (i in globalProps) {
if (globalProps.hasOwnProperty(i)) {
if (type === "classes") {
// Global classes
_props.raw[i] = globalProps[i].replace(/{ns}/g, namespace);
_props[i] = globalProps[i].replace(/{ns}/g, "." + namespace);
} else {
// Global events
_props.raw[i] = globalProps[i].replace(/.{ns}/g, "");
_props[i] = globalProps[i].replace(/{ns}/g, namespace);
}
}
}
return _props;
}
// Set Browser Prefixes
function setBrowserPrefixes() {
var transitionEvents = {
"WebkitTransition": "webkitTransitionEnd",
"MozTransition": "transitionend",
"OTransition": "otransitionend",
"transition": "transitionend"
},
transitionProperties = [
"transition",
"-webkit-transition"
],
transformProperties = {
'transform': 'transform',
'MozTransform': '-moz-transform',
'OTransform': '-o-transform',
'msTransform': '-ms-transform',
'webkitTransform': '-webkit-transform'
},
transitionEvent = "transitionend",
transitionProperty = "",
transformProperty = "",
testDiv = document.createElement("div"),
i;
for (i in transitionEvents) {
if (transitionEvents.hasOwnProperty(i) && i in testDiv.style) {
transitionEvent = transitionEvents[i];
Formstone.support.transition = true;
break;
}
}
Events.transitionEnd = transitionEvent + ".{ns}";
for (i in transitionProperties) {
if (transitionProperties.hasOwnProperty(i) && transitionProperties[i] in testDiv.style) {
transitionProperty = transitionProperties[i];
break;
}
}
Formstone.transition = transitionProperty;
for (i in transformProperties) {
if (transformProperties.hasOwnProperty(i) && transformProperties[i] in testDiv.style) {
Formstone.support.transform = true;
transformProperty = transformProperties[i];
break;
}
}
Formstone.transform = transformProperty;
}
// Window resize
function onWindowResize() {
Formstone.windowWidth = Formstone.$window.width();
Formstone.windowHeight = Formstone.$window.height();
ResizeTimer = Functions.startTimer(ResizeTimer, Debounce, handleWindowResize);
}
function handleWindowResize() {
for (var i in Formstone.ResizeHandlers) {
if (Formstone.ResizeHandlers.hasOwnProperty(i)) {
Formstone.ResizeHandlers[i].callback.call(window, Formstone.windowWidth, Formstone.windowHeight);
}
}
}
Formstone.$window.on("resize.fs", onWindowResize);
onWindowResize();
// RAF
function handleRAF() {
if (Formstone.support.raf) {
Formstone.window.requestAnimationFrame(handleRAF);
for (var i in Formstone.RAFHandlers) {
if (Formstone.RAFHandlers.hasOwnProperty(i)) {
Formstone.RAFHandlers[i].callback.call(window);
}
}
}
}
handleRAF();
// Sort Priority
function sortPriority(a, b) {
return (parseInt(a.priority) - parseInt(b.priority));
}
// Document Ready
Formstone.Ready(function() {
Formstone.$body = $("body");
$("html").addClass( (Formstone.support.touch) ? "touchevents" : "no-touchevents" );
// Viewport
$ViewportMeta = $('meta[name="viewport"]');
ViewportMetaOriginal = ($ViewportMeta.length) ? $ViewportMeta.attr("content") : false;
ViewportMetaLocked = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
$Ready.resolve();
});
// Custom Events
Events.clickTouchStart = Events.click + " " + Events.touchStart;
// Browser Prefixes
setBrowserPrefixes();
window.Formstone = Formstone;
return Formstone;
})
);