You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
15 KiB
JavaScript
565 lines
15 KiB
JavaScript
/*!
|
|
* intention.js Library v0.9.7.2
|
|
* http://intentionjs.com/
|
|
*
|
|
* Copyright 2011, 2013 Dowjones and other contributors
|
|
* Released under the MIT license
|
|
*
|
|
*/
|
|
|
|
(function(root, factory) {
|
|
|
|
'use strict';
|
|
|
|
if (typeof define === 'function' && define.amd) {
|
|
define('intention', ['jquery', 'underscore'], factory);
|
|
} else {
|
|
root.Intention = factory(root.jQuery, root._);
|
|
}
|
|
}(this, function($, _) {
|
|
'use strict';
|
|
|
|
var Intention = function(params){
|
|
var intent = _.extend(this, params,
|
|
{_listeners:{}, contexts:[], elms:$(), axes:{}, priority:[]});
|
|
|
|
return intent;
|
|
};
|
|
|
|
Intention.prototype = {
|
|
|
|
// public methods
|
|
responsive:function responsive(contexts, options){
|
|
// for generating random ids for axis when not specified
|
|
var idChars = 'abcdefghijklmnopqrstuvwxyz0123456789',
|
|
id='', i;
|
|
|
|
// create a random id for the axis
|
|
for(i=0; i<5; i++){
|
|
id += idChars[Math.floor(Math.random() * idChars.length)];
|
|
}
|
|
var defaults = {
|
|
// if no matcher function is specified expect to compare a
|
|
// string to the ctx.name property
|
|
matcher: function(measure, ctx){
|
|
return measure === ctx.name;
|
|
},
|
|
// function takes one arg and returns it
|
|
measure: _.identity,
|
|
ID: id
|
|
};
|
|
|
|
if(_.isObject(options) === false) {
|
|
options = {};
|
|
}
|
|
|
|
if((_.isArray(contexts)) && (_.isArray(contexts[0].contexts))){
|
|
_.each(contexts, function(axis){
|
|
responsive.apply(this, axis);
|
|
}, this);
|
|
return;
|
|
}
|
|
|
|
if((_.isArray(contexts) === false) && _.isObject(contexts)){
|
|
options = contexts;
|
|
} else {
|
|
options.contexts = contexts;
|
|
}
|
|
|
|
// fill in the options
|
|
options = _.extend({}, defaults, options);
|
|
|
|
// bind an the respond function to the axis ID and prefix it
|
|
// with an underscore so that it does not get whomped accidentally
|
|
this.on('_' + options.ID + ':', _.bind(
|
|
function(e){
|
|
this.axes = this._contextualize(
|
|
options.ID, e.context, this.axes);
|
|
this._respond(this.axes, this.elms);
|
|
|
|
}, this));
|
|
|
|
var axis = {
|
|
ID:options.ID,
|
|
current:null,
|
|
contexts:options.contexts,
|
|
respond:_.bind(this._responder(options.ID, options.contexts,
|
|
options.matcher, options.measure), this)
|
|
};
|
|
|
|
this.axes[options.ID] = axis;
|
|
|
|
this.axes.__keys__ = this.priority;
|
|
|
|
this.priority.unshift(options.ID);
|
|
|
|
return axis;
|
|
},
|
|
|
|
elements: function(scope){
|
|
|
|
// find all responsive elms in a specific dom scope
|
|
if(!scope){
|
|
scope = document;
|
|
}
|
|
|
|
$('[data-intent],[intent],[data-in],[in]',
|
|
scope).each(_.bind(function(i, elm){
|
|
this.add($(elm));
|
|
}, this));
|
|
|
|
return this;
|
|
},
|
|
|
|
add: function(elms, options){
|
|
|
|
var spec;
|
|
|
|
if(!options) {
|
|
options = {};
|
|
}
|
|
|
|
// is expecting a jquery object
|
|
elms.each(_.bind(function(i, elm){
|
|
var exists = false;
|
|
this.elms.each(function(i, respElm){
|
|
if(elm === respElm) {
|
|
exists=true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if(exists === false){
|
|
// create the elements responsive data
|
|
spec = this._fillSpec(
|
|
_.extend(options, this._attrsToSpec(elm.attributes, this.axes)));
|
|
// make any appropriate changes based on the current contexts
|
|
this._makeChanges($(elm), spec, this.axes);
|
|
|
|
this.elms.push({
|
|
elm: elm,
|
|
spec: spec
|
|
});
|
|
}
|
|
|
|
}, this));
|
|
|
|
return this;
|
|
},
|
|
|
|
remove: function(elms){
|
|
// is expecting a jquery object
|
|
var respElms = this.elms;
|
|
// elms to remove
|
|
elms.each(function(i, elm){
|
|
// elms to check against
|
|
respElms.each(function(i, candidate){
|
|
if(elm === candidate.elm){
|
|
respElms.splice(i, 1);
|
|
// found the match, break the loop
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
});
|
|
return this;
|
|
},
|
|
|
|
is: function(ctxName){
|
|
var axes = this.axes;
|
|
return _.some(axes.__keys__, function(key){
|
|
return ctxName === axes[key].current;
|
|
});
|
|
},
|
|
|
|
current: function(axisName){
|
|
if(this.axes.hasOwnProperty(axisName)){
|
|
return this.axes[axisName].current;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// code and concept taken from simple implementation of
|
|
// observer pattern outlined here:
|
|
// http://www.nczonline.net/blog/2010/03/09/custom-events-in-javascript/
|
|
on: function(type, listener){
|
|
|
|
var events = type.split(' '),
|
|
i=0;
|
|
|
|
for(i;i<events.length;i++){
|
|
if(this._listeners[events[i]] === undefined) {
|
|
this._listeners[events[i]]=[];
|
|
}
|
|
this._listeners[events[i]].push(listener);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
off: function(type, listener){
|
|
if(_.isArray(this._listeners[type])){
|
|
var listeners = this._listeners[type],
|
|
i;
|
|
for(i=0;listeners.length; i++){
|
|
if(listeners[i] === listener){
|
|
listeners.splice(i,1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// privates
|
|
_responder: function(axisID, contexts, matcher, measure){
|
|
|
|
var currentContext;
|
|
|
|
// called to perform a check
|
|
return function(){
|
|
|
|
var measurement = measure.apply(this, arguments);
|
|
|
|
_.every(contexts, function(ctx){
|
|
if( matcher(measurement, ctx)) {
|
|
// first time, or different than last context
|
|
if( (currentContext===undefined) ||
|
|
(ctx.name !== currentContext.name)){
|
|
|
|
currentContext = ctx;
|
|
|
|
// event emitting!
|
|
// emit the private axis event
|
|
this._emitter(
|
|
{_type: '_' + axisID + ':', context:currentContext.name},
|
|
currentContext, this)
|
|
|
|
// emit the public axis event
|
|
._emitter({_type: axisID + ':', context:currentContext.name},
|
|
currentContext, this)
|
|
|
|
// attempt to trigger the axis to context pair
|
|
._emitter(_.extend({},
|
|
{_type: axisID + ':' + currentContext.name},
|
|
currentContext), currentContext, this)
|
|
|
|
// then emit the context event (second ensures the context
|
|
// changes happen after all dom manipulations)
|
|
._emitter(_.extend({}, {_type:currentContext.name},
|
|
currentContext), currentContext, this);
|
|
|
|
// done, break the loop
|
|
return false;
|
|
}
|
|
// same context, break the loop
|
|
return false;
|
|
}
|
|
return true;
|
|
}, this);
|
|
|
|
// return the intention object for chaining
|
|
return this;
|
|
};
|
|
},
|
|
|
|
_emitter: function(event){
|
|
if(typeof event === 'string') {
|
|
event={_type:event};
|
|
}
|
|
if(!event.target){
|
|
event.target=this;
|
|
}
|
|
if(!event._type){
|
|
throw new Error(event._type + ' is not a supported event.');
|
|
}
|
|
if(_.isArray(this._listeners[event._type])){
|
|
var listeners = this._listeners[event._type],
|
|
i;
|
|
for(i=0; i<listeners.length; i++){
|
|
listeners[i].apply(this, arguments);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
_fillSpec: function(spec){
|
|
|
|
var applySpec = function(fn){
|
|
_.each(spec, function(axisOptions, axis){
|
|
_.each(axisOptions, function(ctxOptions, ctx){
|
|
fn(ctxOptions, ctx, axis);
|
|
});
|
|
});
|
|
}, filler={};
|
|
|
|
applySpec(function(options){
|
|
// check to see if the ctx val is an object, could be a string
|
|
if(_.isObject(options)){
|
|
_.each(options, function(val, func){
|
|
filler[func] = '';
|
|
});
|
|
}
|
|
});
|
|
|
|
applySpec(function(options, ctx, axis){
|
|
if(_.isObject(options)){
|
|
spec[axis][ctx] = _.extend({}, filler, options);
|
|
}
|
|
});
|
|
|
|
return spec;
|
|
},
|
|
|
|
_assocAxis: function(ctx, axes){
|
|
|
|
var match=false;
|
|
|
|
_.every(axes.__keys__, function(axis){
|
|
|
|
if(match === false){
|
|
_.every(axes[axis].contexts, function(ctxCandidate){
|
|
if(ctxCandidate.name === ctx){
|
|
match = axis;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return true;
|
|
}else {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return match;
|
|
},
|
|
|
|
_makeSpec: function(axis, ctx, sAttr, value, spec){
|
|
var axisObj,
|
|
ctxObj;
|
|
|
|
if(spec[axis] !== undefined){
|
|
axisObj = spec[axis];
|
|
|
|
if(axisObj[ctx] === undefined) {
|
|
axisObj[ctx] = {};
|
|
}
|
|
} else {
|
|
axisObj = {};
|
|
axisObj[ctx] = {};
|
|
spec[axis] = axisObj;
|
|
}
|
|
axisObj[ctx][sAttr] = value;
|
|
|
|
return spec;
|
|
},
|
|
|
|
_attrsToSpec: function(attrs, axes){
|
|
|
|
var spec={},
|
|
fullPattern = new RegExp(
|
|
'^(data-)?(in|intent)-(([a-zA-Z0-9][a-zA-Z0-9]*:)?([a-zA-Z0-9]*))-([A-Za-z:-]+)'),
|
|
axisPattern = new RegExp(
|
|
'^(data-)?(in|intent)-([a-zA-Z0-9][_a-zA-Z0-9]*):$');
|
|
|
|
_.each(attrs, function(attr){
|
|
|
|
var specMatch = attr.name.match(fullPattern),
|
|
axisName;
|
|
|
|
if(specMatch !== null) {
|
|
|
|
specMatch = specMatch.slice(-3);
|
|
axisName = specMatch[0];
|
|
|
|
if(specMatch[0] === undefined){
|
|
|
|
// if there is no axis find one:
|
|
specMatch[0] = this._assocAxis(specMatch[1], axes);
|
|
|
|
if(specMatch[0] === false) {
|
|
// there is no context, so get outa here
|
|
return; // skipt the attr
|
|
}
|
|
} else {
|
|
specMatch[0] = specMatch[0].replace(/:$/, '');}
|
|
|
|
specMatch.push(attr.value);
|
|
specMatch.push(spec);
|
|
|
|
spec = this._makeSpec.apply(this, specMatch);
|
|
|
|
} else if(axisPattern.test(attr.name)){
|
|
|
|
axisName = attr.name.match(axisPattern)[3];
|
|
|
|
_.each(axes[axisName].contexts,
|
|
function(context){
|
|
this._makeSpec(axisName, context.name, 'class', context.name +
|
|
' ' + attr.value, spec);
|
|
},
|
|
this);}},
|
|
this);
|
|
|
|
return spec;
|
|
},
|
|
|
|
_contextSpec: function(ctxObj, specs){
|
|
if(specs.hasOwnProperty(ctxObj.axis) &&
|
|
specs[ctxObj.axis].hasOwnProperty(ctxObj.ctx)){
|
|
return specs[ctxObj.axis][ctxObj.ctx];
|
|
}
|
|
return {};
|
|
},
|
|
_resolveSpecs: function(currentContexts, specs){
|
|
|
|
var changes={},
|
|
moveFuncs=['append', 'prepend', 'before', 'after'];
|
|
|
|
_.each(currentContexts, function(ctxObj){
|
|
// if the axis or the context to not exist in the specs object
|
|
// skip to the next one
|
|
_.each(this._contextSpec(ctxObj, specs), function(val, func){
|
|
|
|
if(func==='class'){
|
|
if(!changes[func]){
|
|
changes[func] = [];
|
|
}
|
|
changes[func] = _.union(changes[func], val.split(' '));
|
|
|
|
} else if(((changes.move === undefined) ||
|
|
(changes.move.value === '')) &&
|
|
($.inArray(func, moveFuncs) !== -1)){
|
|
|
|
changes.move = {value:val, placement:func};
|
|
|
|
} else {
|
|
if((changes[func] === undefined) || (changes[func] === '')){
|
|
changes[func]=val;
|
|
}
|
|
}
|
|
}, this);
|
|
}, this);
|
|
return changes;
|
|
},
|
|
|
|
_currentContexts: function(axes) {
|
|
var contexts = [];
|
|
|
|
_.each(axes.__keys__, function(ID){
|
|
if(axes[ID].current !== null) {
|
|
contexts.push({ctx:axes[ID].current, axis:ID});
|
|
return;
|
|
}
|
|
});
|
|
|
|
return contexts;
|
|
},
|
|
|
|
_removeClasses: function(specs, axes) {
|
|
|
|
var toRemove = [];
|
|
|
|
_.each(axes.__keys__, function(key){
|
|
|
|
var axis = axes[key];
|
|
|
|
_.each(axis.contexts, function(ctx){
|
|
|
|
// ignore the current context, those classes SHOULD be applied
|
|
if(ctx.name === axis.current) {
|
|
return;
|
|
}
|
|
var contextSpec = this._contextSpec(
|
|
{axis:axis.ID, ctx:ctx.name}, specs),
|
|
classes;
|
|
|
|
if(contextSpec !== undefined) {
|
|
if(contextSpec['class'] !== undefined) {
|
|
classes = contextSpec['class'].split(' ');
|
|
if(classes !== undefined){
|
|
toRemove = _.union(toRemove, classes);
|
|
}
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
}, this);
|
|
|
|
return toRemove;
|
|
},
|
|
|
|
_contextConfig: function(specs, axes){
|
|
|
|
return this._resolveSpecs(this._currentContexts(axes), specs, axes);
|
|
},
|
|
|
|
_makeChanges: function(elm, specs, axes){
|
|
|
|
if(_.isEmpty(axes)===false){
|
|
var ctxConfig = this._contextConfig(specs, axes);
|
|
|
|
_.each(ctxConfig, function(change, func){
|
|
if(func==='move'){
|
|
if( (specs.__placement__ !== change.placement) ||
|
|
(specs.__move__ !== change.value)){
|
|
|
|
$(change.value)[change.placement](elm);
|
|
|
|
// save the last placement of the element so
|
|
// we're not moving it around for no good reason
|
|
specs.__placement__ = change.placement;
|
|
specs.__move__ = change.value;
|
|
}
|
|
} else if(func === 'class') {
|
|
|
|
var classes = elm.attr('class') || '';
|
|
|
|
// the class add/remove formula
|
|
classes = _.union(change,
|
|
_.difference(classes.split(' '),
|
|
this._removeClasses(specs, axes)));
|
|
|
|
elm.attr('class', classes.join(' '));
|
|
|
|
} else {
|
|
elm.attr(func, change);
|
|
}
|
|
}, this);
|
|
}
|
|
return elm;
|
|
},
|
|
|
|
_respond: function(axes, elms){
|
|
// go through all of the responsive elms
|
|
elms.each(_.bind(function(i, elm){
|
|
var $elm = $(elm.elm);
|
|
this._makeChanges($elm, elm.spec, axes);
|
|
$elm.trigger('intent', this);
|
|
}, this));
|
|
},
|
|
|
|
_contextualize: function(axisID, context, axes){
|
|
axes[axisID].current = context;
|
|
return axes;
|
|
},
|
|
|
|
// private props
|
|
|
|
// axis test, does it begin with an underscore? for testing inside
|
|
// spec objects
|
|
_axis_test_pattern: new RegExp("^_[a-zA-Z0-9]"),
|
|
|
|
// match a group after the underscore:
|
|
_axis_match_pattern: new RegExp("^_([a-zA-Z0-9][_a-zA-Z0-9]*)"),
|
|
|
|
// simple trim
|
|
_trim_pattern:new RegExp( "^\s+|\s+$", "g" )
|
|
};
|
|
|
|
return Intention;
|
|
}));
|