essential.js
// EssentialJS v0.7.0
// Copyright (c)2014 - 2016 Roberto Dip
// Distributed under MIT license
// http://roperzh.github.io/essential.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.returnExports = factory();
}
}(this, function () {
var Essential = {
rootElement: document,
Core: {},
// Start
// -----
//
// since v0.1.0
//
// A wrapper of `#Essential.loadBehaviors`, this method is deprecated
// direct usage of `loadBehaviors` is encouraged.
//
// Param application[`Object`] an object containing behaviors names as a key
// and behaviors objects as a value.
start: function(application) {
this.loadBehaviors({
application: application
});
},
// Load Behaviors
// --------------
//
// since v0.5.0
//
// Wakes up the engine, searching and attaching
// behaviors with their proper elements
//
// Param options[`Object`] allows the follwing values:
// - `application`[`Object`] an object containing behaviors names as a key
// and behaviors objects as a value
// - `context` [`DOMElement`] context to look for behaviors.
// If no context is provided the default is `Essential.rootElement`
//
// **Example**
//
// ```javascript
// MyApp = {};
// MyApp.Carousel = Essential.Behaviors.extend();
// Essential.loadBehaviors({ application: MyApp, context: document });
// // will attach the carousel behavior to proper elements
// ```
loadBehaviors: function(options) {
options.context = options.context || this.rootElement;
var initializedBehaviors =
this.initializeBehaviors(options.application, options.context);
this.launchBehaviors(initializedBehaviors);
},
// Initialize Behaviors
// --------------------
//
// Crawls an element looking for behaviors and call `#new` on every behavior
// found with `lateStart = true`, so the behaviors are initialized, but
// there is no event delegation
//
// param application [`Object`] object containing behaviors to be initialized
//
// param element [`DomeElement`] context to look for declared behaviors
initializeBehaviors: function(application, element) {
var behaviorsInDOM = this.Core.crawl(element),
rawBehaviorsNames = Object.keys(behaviorsInDOM),
initializedBehaviors = [],
i = -1;
while(rawBehaviorsNames[++i]) {
var rawName = rawBehaviorsNames[i],
name = this.Core.camelize(rawName),
behavior = application[name];
if(typeof behavior !== "undefined") {
var elementsWithBehavior = behaviorsInDOM[rawName],
j = -1;
while(elementsWithBehavior[++j]) {
var initializedBehavior = behavior.new(elementsWithBehavior[j], true);
initializedBehaviors.push(initializedBehavior);
}
}
}
return initializedBehaviors;
},
// Launch Behaviors
// ----------------
//
// Given a list of behaviors, this method sort these based on their
// `priority` value, and then call `#start` on every one
//
// param behaviorList[`Array<Object>`] an array containing behaviors already
// initialized
launchBehaviors: function(behaviorList) {
var sortedBehaviors = behaviorList.sort(this.Core.SortMethods.byPriority),
i = -1;
while(sortedBehaviors[++i]) {
sortedBehaviors[i].start();
}
}
};
/*!
* Includes proto-js by Axel Rauschmayer
* https://github.com/rauschma/proto-js
*/
if (!Object.getOwnPropertyDescriptors) {
Object.getOwnPropertyDescriptors = function (obj) {
var descs = {};
Object.getOwnPropertyNames(obj).forEach(function (propName) {
descs[propName] = Object.getOwnPropertyDescriptor(obj, propName);
});
return descs;
};
}
var Proto = {
new: function () {
var instance = Object.create(this);
if (instance.constructor) {
instance.constructor.apply(instance, arguments);
}
return instance;
},
extend: function (subProps) {
var subProto = Object.create(this, Object.getOwnPropertyDescriptors(subProps));
subProto.super = this;
return subProto;
}
};
Function.prototype.extend = function (subProps) {
var constrFunc = this;
var tmpClass = Proto.extend.call(constrFunc.prototype, Proto);
return tmpClass.extend(subProps);
};
// Custom Event Polyfill
// ---------------------
//
// since 0.5.0
//
// source: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
//
// Allows the usage of custom events on IE 9 - 10
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
};
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
// Behavior
// --------
//
// Represents a behavior of some element or group of elements.
// The objetive is define a set of rules and events which
// can be associated to an element and reutilized later on
//
// When a behavior is defined, a hash of events must be defined too,
// and on initialization a DOM element must be provided
//
// Also you can define an `init` function, which is always called when the
// behavior is initialized
//
// **Example**
// ```javascript
// Carousel = Essential.Behavior.extend({
// events: {
// "click .next": "goToNextSlide"
// },
//
// init: function() {
// // Called on behavior initialization
// },
//
// goToNextSlide: function(e) {
// //...
// }
// });
//
// var carousel = Carousel.new(domElement);
// ```
Essential.Behavior = Proto.extend({
constructor: function(domElement, lateStart, initParams) {
this.el = domElement;
// A behavior can be initialized without attaching events with the `lateStart`
// flag, if it is present the methods `delegateEvents` and `ìnit` are omitted
// but can be called later with `start`
//
// **Example**
// ```javascript
// carousel = new Carousel(domElement, true);
// // delegateEvents and init not called
//
// carousel.start();
// // delegateEvents and init called
// ```
if(!lateStart) {
this.start(initParams);
}
},
start: function(initParams) {
if(typeof this.init === "function") {
this.init(initParams);
}
this.delegateEvents();
this.listenChannels();
},
// Delegate Events
// ---------------
//
// since v0.1.0
//
// Delegates events declared in `this.events`, using `this.el` as a context
delegateEvents: function() {
Essential.Core.mapEvents.call(this, this.events, this.el);
},
// Listen Channels
// ---------------
//
// since v0.5.0
//
// Attach event handlers to channels declared in `this.channels using
// `document` as a context
listenChannels: function() {
Essential.Core.mapEvents.call(this, this.channels, document);
},
// Emit
// ----
//
// Facilitates the emission of custom events through the CustomEvent
// Interface. IE9 and IE10 are supported via polyfill
//
// since v0.5.0
//
// param dataset[`Object`] valid dataset values are:
//
// - channel: [`String`] name (identifier) of the channel
//
// - context: [`DOMElement`] DOM context in which the event is triggered,
// this parameter can be ommited. Default value is `document`
//
// - bubles: [`Boolean`] defines if this event should bubble or not,
// defaults to true
//
// - cancelable: [`Boolean`] indecates whether the event is cancelable,
// defaults to false
//
// - data: [`Object`] data to be included in the `"detail"` key of the
// event can be accesed later via `event.detail`
// (check the CustomEvent spec for more info)
emit: function(dataset) {
dataset.context = dataset.context || this.el;
dataset.data = dataset.data || {};
dataset.bubbles = dataset.bubbles || true;
dataset.cancelable = dataset.cancelable || false;
var customEvent = new CustomEvent(dataset.channel, {
"bubbles": dataset.bubbles,
"cancelable": dataset.cancelable,
"detail": dataset.data
});
dataset.context.dispatchEvent(customEvent);
},
priority: 0
});
// Map Events
// ----------
//
// since v0.5.0
//
// Given a document context, maps a hash of events to all ocurrences
// in the context using the DOM Event Interface
//
// param events[`Object`] key-value map which follows some conventions:
//
// - key: must be a String, containing the event name. Optionally after the event
// name a valid CSS selector must be placed, for example `"click #element"`
//
// - value: must be a name of a funciton pertaining to the current in which
// `mapEvents` its executed
//
// param context[`DOMElement`] element to search through
//
// **Example**
// ```javascript
// var events = {
// "click .next": "goToNextSlide"
// };
//
// Essential.Core.mapEvents(events, document);
// ```
Essential.Core.mapEvents = function(events, context) {
if(typeof events === "undefined") {
return;
}
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
for(var key in events) {
var method = events[key];
var match = key.match(delegateEventSplitter);
var eventName = match[1],
selector = match[2],
nodeList = selector ? context.querySelectorAll(selector) : [context];
if(typeof this[method] === "undefined") {
continue;
}
Essential.Core.bind(eventName, nodeList, this[method].bind(this));
}
};
// Bind
// ----
//
// Binds an event to a node
//
// Param eventName[`String`] name of the event to be binded
//
// Param callback[`Function`] function to be called when the event is triggered
//
// Param nodeList[`NodeList`, `Array`] node elements to be binded
//
// **Example**
//
// ```javascript
// var nodeList = document.querySelectorAll("*");
//
// Essential.Core.bind("hover", nodeList, function() {
// alert("hover!");
// });
//
// // If the hover event is triggered for any of the
// // elements in the nodeList the alert will appear
// ```
Essential.Core.bind = function(eventName, nodeList, callback) {
var i = -1;
while(nodeList[++i]) {
var currentElement = nodeList[i];
if(currentElement.addEventListener) {
nodeList[i].addEventListener(eventName, callback);
} else {
currentElement.attachEvent("on" + eventName, callback);
}
}
};
// RegExp Helpers
// --------------
// Looks for some of this characters `:` `-` `_` the objetive is allow
// to define behaviors like `cool:carousel` or `small-carousel`
Essential.Core.SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
// Finds the first letter of a given string
Essential.Core.FIRST_LETTER_REGEXP = /^[a-z]/g;
// Camelize
// --------
//
// Converts strings to UpperCamelCase
//
// Param name[`String`] the name to be camelized
//
// Returns `String` camel case representation of the name
//
// **Example**
//
// ```javascript
// Essential.Core.camelize("cool-carousel");
//
// // => CoolCarousel
// ```
Essential.Core.camelize = function(name) {
return name.
replace(Essential.Core.FIRST_LETTER_REGEXP, function(letter) {
return letter.toUpperCase();
}).
replace(Essential.Core.SPECIAL_CHARS_REGEXP, function(_, separator, letter) {
return letter.toUpperCase();
});
};
// Crawl
//------
//
// Scans the DOM looking for behaviors
//
// Return `Array<Object>` an array of objects with the behavior name as
// a key and an array of DOM nodes as a value
//
// **Example**
//
// ```html
// <div behavior="carousel"></div>
// ```
//
// ```javascript
// Essential.Core.crawl();
//
// // => [{ carousel: [<HTMLDivElement>, <HTMLDivElement>] }]
// ```
Essential.Core.crawl = function(rootElement) {
var all = rootElement.querySelectorAll("[data-behavior], [behavior]"),
i = -1,
result = {};
while(all[++i]) {
var currentElement = all[i],
rawBehaviors = currentElement.getAttribute("data-behavior") || currentElement.getAttribute("behavior"),
behaviorsList = rawBehaviors.split(" "),
j = -1;
while(behaviorsList[++j]) {
var currentBehavior = behaviorsList[j];
if(result[currentBehavior]) {
result[currentBehavior].push(currentElement);
} else {
result[currentBehavior] = [currentElement];
}
}
}
return result;
};
// Sort Methods
// ------------
//
// Namespace to hold sort methods
Essential.Core.SortMethods = {
// By Priority
// -----------
//
// This criteria allows to sort behaviors based on their respective priorities,
// in descending order, that means behaviors with bigger priority will appear
// first
byPriority: function(behaviorA, behaviorB) {
return behaviorB.priority - behaviorA.priority;
}
};
return Essential;
}));