UNPKG

cortex.js

Version:

Cortex, the application framework that tickles your senses

1,733 lines (1,479 loc) 150 kB
/* [square] Bundle: /lib/cortex.js */ /*globals Plates */ /** * @TODO implement options.silence so we can silence pointless emit calls */ (function (root, undefined) { "use strict"; // save local references to slice var slice = Array.prototype.slice , toString = Object.prototype.toString , documentElement = document.documentElement , uniqueId = 0 , _Cortex = root.Cortex , _$ = root.$; // Create the top level namespace. All plugins and dependencies will be // attached to this. var Cortex = root.Cortex = {}; /** * Expose the current version number of the Cortex Library. * * @type {String} * @api public */ Cortex.version = '0.0.0'; /** * Simple conflict handling if we are overriding the $ or other Cortex * instances. Basically, if you need this function you are doing something * wrong. * * @param {Boolean} selector $ selector only * @api public */ Cortex.noConflict = function noConflict(selector) { if (selector) { if (_$) root.$ = _$; } else { if (_Cortex) root.Cortex = _Cortex; } }; /** * Simple type checking. * * @param {Mixed} type * @param {String} expected * @returns {Mixed} */ Cortex.is = function is(type, expected) { var instance = toString.call(type).slice(8, -1).toLowerCase(); return expected ? instance === expected : instance; }; /** * Cortex.forEach is a simple iterator. Iterations should be delegated to the * Native functions when ever possible. * * @param {Mixed} collection * @param {Function} iterator * @param {Mixed} context * @api public */ Cortex.forEach = Cortex.each = function forEach(collection, iterator, context) { if (!collection) return; var i = 0; if ('forEach' in collection) { collection.forEach(iterator, context); } else if (Array.isArray(collection)) { for (var l = collection.length; i < l; i++) { if (iterator.call(context, collection[i], i, collection) === false) break; } } else { for (i in collection) { if (iterator.call(context, collection[i], i, collection) === false) break; } } return this; }; /** * Cortex.filter returns all elements that pass the truth test. Filtering * should be delegated to native function when ever possible. * * @param {Mixed} collection * @param {Function} iterator * @param {Mixed} context * @returns {Array} * @api pubic */ Cortex.filter = Cortex.select = function filter(collection, iterator, context) { var results = []; if (!collection) return results; if ('filter' in collection) return collection.filter(iterator, context); Cortex.forEach(collection, function forEach(value) { if (!iterator.apply(context, arguments)) results.push(value); }); return results; }; /** * Cortex.map returns the results of applying the iterator to each item in the * collection. * * @param {Mixed} collection * @param {Function} iterator * @param {Mixed} context * @returns {Array} * @api public */ Cortex.map = Cortex.collect = function map(collection, iterator, context) { var results = []; if (!collection) return results; if ('map' in collection && typeof collection.map === 'function') { return collection.map(iterator, context); } Cortex.forEach(collection, function forEach(value, index, list) { results.push(iterator.call(context, value, index, list)); }); return results; }; /** * MatchesSelector, simple checking if a DOM element matches a certain CSS * selector. This method is used internally for Event Listening. * * @param {String} selector * @param {DOM} element * @returns {Boolean} */ Cortex.matchesSelector = function matchesSelector(selector, element) { if (element.nodeType !== 1) return false; if (Cortex._matchesSelector) return Cortex._matchesSelector.call(element, selector); return Cortex.Sizzle.matchesSelector(element, selector); }; // attempt to find a native selector Cortex._matchesSelector = documentElement.matchesSelector || documentElement.mozMatchesSelector || documentElement.webkitMatchesSelector || documentElement.oMatchesSelector || documentElement.msMatchesSelector; /** * Extend the given object with all the properties of the supplied arguments. * * @param {Object} obj the base object that needs to be extended with all args * @returns {Object} obj */ Cortex.extend = function extend(obj) { Cortex.forEach(slice.call(arguments, 1), function iterator(merge) { for (var prop in merge) obj[prop] = merge[prop]; }); return obj; }; /** * Helper function to get the value from an object as property. If the * property is a function it will be executed instead. * * @param {Object} obj * @param {String} prop * @returns {Mixed} */ Cortex.property = function property(obj, prop) { return obj && prop in obj ? (typeof obj[prop] === 'function' ? obj[prop]() : obj[prop]) : null; }; /** * Cortex.EventEmitter: * * Cortex.EventEmitter is heavily influenced by the Node.js EventEmitter * module and the regular Observer pattern. It does have some level of * compatibility with the Node.js EventEmitter but don't depend on the same * behaviour, the API's are just the same. * * @type {Object} */ var eventsplitter = /\,\s+/; var EventEmitter = Cortex.EventEmitter = { /** * Adds an event listener. * * @param {String} events space separated list of events to listen on * @param {Function} callback * @param {Mixed} context * @api public */ on: function on(event, callback, context) { var events = eventsplitter.test(event) ? event.split(eventsplitter) : [event]; this.events = this.events || (this.events = {}); while (event = events.shift()) { if (!event) continue; (this.events[event] = this.events[event] || []).push({ context: context , callback: callback }); } return this; } /** * Removes attached event listeners. If no arguments are supplied it will * remove all the assigned events. If no callback is supplied it will * remove all listeners for the specified events. If no context is * provided it will remove all event listeners that have the supplied * callback. * * @param {String} event * @param {Function} callback * @param {Mixed} context * @api public */ , off: function off(event, callback, context) { var events = eventsplitter.test(event) ? event.split(eventsplitter) : [event] , args = arguments.length , handles, handle; // short cut, removes all assigned events if (!args || !this.events) { this.events = {}; return this; } /** * Helper function to filter out the `this.events` array * * @param {Function} handle the callback of the event * @api private */ function remove(handle) { return !( handle.callback === callback && (!context ? true : handle.context === context) ); } while (event = events.shift()) { if (!this.events[event] || !event) continue; if (!callback) { delete this.events[event]; continue; } this.events[event] = this.events[event].filter(remove); } return this; } /** * Call the supplied event listeners with all the supplied arguments. * * @param {String} event * @returns {Boolean} successful emit * @api public */ , emit: function emit(event) { if (this.suppressed || !this.events) return false; var events = eventsplitter.test(event) ? event.split(eventsplitter) : [event] || Object.keys(this.events) , args = slice.call(arguments, 1) , called = false; /** * Helper function that is called on each iteration of matching events. * * @param {Function} handle * @api private */ function call(handle) { handle.callback.apply(handle.context, args); called = true; } while (event = events.shift()) { if (!this.events[event] || !event) continue; this.events[event].forEach(call); } return called; } /** * Create a listeners that is only called once. * * @param {String} event * @param {Function} callback * @param {Mixed} context * @api public */ , once: function once(event, handle, context) { var self = this; /** * Create a simple event handler that will remove it self from all * assigned events once it's fired. * * @api private */ function one() { self.off(event, one); handle.apply(this, arguments); } one.handle = handle; return this.on(event, one, context); } /** * Enable the EventEmitter. * * @api public */ , enabled: function enabledEmitter() { this.suppressed = this.suppressed || 0; if (--this.suppressed < 0) this.suppressed = 0; return this; } /** * Disable the EventEmitter. * * @api public */ , disable: function disableEmitter() { if (this.suppressed) ++this.suppressed; return this; } }; // EventEmitter method aliases, to maintain API compatibility with EventEmitter EventEmitter.addListener = EventEmitter.on; EventEmitter.removeListener = EventEmitter.off; /** * The Cortex.Structure * * The Cortex Model is a light-weight implementation of the Backbone.js model * and is designed to be used a wrapper for the DOM and for Objects so we have * one single universal structure for all operations. Which will hopefully help * us to keep our code sane. */ var Structure = Cortex.Structure = function Structure(attributes, options) { var defaults; // check if the supplied attributes need to be parsed before we can use them. attributes = this.parse(attributes); // check if we have default values in our structure, so we are sure that // these values are always set correctly if (defaults = Cortex.property(this, 'defaults')) { attributes = Cortex.extend({}, defaults, attributes); } Cortex.extend(this, options); this.history = {}; this.id = (uniqueId++) + ':' + (+new Date()); this.attributes = {}; // Only set attributes if we have them, but also for every other type if (!Cortex.is(attributes, 'object') || Object.keys(attributes).length) { this.set(attributes, { silent: true }); } if (this.initialize) { this.initialize.apply(this, arguments); } }; Cortex.extend(Structure.prototype, EventEmitter, { /** * Get attributes from the structure * * @param {String} attr * @returns {Mixed} * @api public */ get: function get(attr) { if (!~attr.indexOf('.')) return this.attributes[attr]; var result = attr , structure = this.attributes; for (var paths = attr.split('.'), i = 0, length = paths.length; i < length && structure; i++) { result = structure[+paths[i] || paths[i]]; structure = result; } return result || this.attributes[attr]; } /** * See if we have this attribute. We use the #get method for checking as * it's easier to extend the structure this way. * * @param {String} attr * @returns {Boolean} * @api private */ , has: function has(attr) { return !!this.get(attr); } /** * Set new attributes in the structure. * * @param {String} key * @param {Mixed} value * @param {Object} options * @api public */ , set: function set(key, value, options) { var attributes; // support bulk updates, by allowing objects to set as a whole if (typeof key === 'object') { attributes = key; options = value; } else { attributes = {}; attributes[key] = value; } options = options || {}; if (!attributes) return this; if (attributes instanceof Structure) attributes = attributes.plain(); var current = this.attributes , cortex = this , changed = {} , added = {}; Cortex.forEach(attributes, function update(value, key) { if (options.unset) return cortex.remove(key); if (key in current && current[key] !== value) changed[key] = true; if (!(key in current)) added[key] = true; // save the current version of the key in the history, so we can undo // changes if needed cortex.history[key] = current[key]; current[key] = value; }); Cortex.forEach(changed, function eachChange(value, key) { cortex.emit('changed:' + key, current[key], cortex.attributes[key]); cortex.emit('changed', key, current[key], cortex.attributes[key]); }); Cortex.forEach(added, function eachAdded(value, key) { cortex.emit('added:' + key, value); cortex.emit('added', key, value); }); this.attributes = current; return this; } /** * Remove attributes from the structure. * * @param {String} attr * @api public */ , remove: function remove(key) { var value = this.attributes[key]; // remove it from all our internal data structures delete this.attributes[key]; delete this.history[key]; this.emit('deleted:' + key, value); this.emit('deleted', key, value); return this; } /** * Clear all attributes of the structure. * * @api public */ , clear: function clear(options) { (options || (options = {})).unset = true; return this.set(this.attributes, options); } /** * Create a new clone of the model that is identical to this one. * * @api public */ , clone: function clone() { return new this.constructor(this.attributes); } /** * Get the previous attributes. * * @param {String} attr attribute that we want the old value from. * @api public */ , previous: function previous(attr) { return this.history[attr]; } /** * Parse converts the given attributes of an Structure to an Object. * * @param {Object} obj * @returns {Object} */ , parse: function parse(obj) { return obj; } /** * Transform the Model to a regular plain JavaScript structure. * * @param {Mixed} index the index of the array or key that it should return * @param {Object} options * @returns {Object} * @api public */ , plain: function plain(index, options) { if (typeof index === 'object') { options = index; index = null; } return index === undefined ? this.attributes : this.attributes[index]; } }); /** * Cortex.Collection: * * Provides a standard collection class for a set of Structure. * * @constructor * @param {Array} structures * @param {Object} options * @api public */ var Collection = Cortex.Collection = function Collection(structures, options) { options = options || {}; Cortex.extend(this, options); this.length = 0; this.structures = []; this.structureId = {}; if (this.initialize) { this.initialize.apply(this, arguments); } if (structures) this.add(structures, { silent: true }); }; Cortex.extend(Collection.prototype, EventEmitter, { /** * The default structure for a collection is just a plain Structure * * @type {Cortex.Structure} */ structure: Structure /** * Transform the collection in a plain object structure. * * @param {Mixed} index * @param {Object} options * @returns {Array} * @api public */ , plain: function plain(index, options) { if (typeof index === 'object') { options = index; index = null; } var results = this.structures.map(function plainStructure(structure) { return structure.plain(options); }); return index !== undefined ? results[index] : results; } /** * Add a new Structure or an Array of Structures to the collections. * * @param {Mixed} structures * @param {Object} options * @api public */ , add: function add(structures, options) { structures = Array.isArray(structures) ? structures.slice() : [structures]; structures = this.parse(structures); options = options || {}; var added = []; for (var i = 0, l = structures.length, structure; i < l; i++) { structure = this.prepare(structures[i], options); // check if we have valid structures if (!structure || structure.id in this.structureId) continue; // prevent duplicates if ('dupe' in this && this.dupe(structure)) continue; this.structureId[structure.id] = structure; this.structures.push(structure); // maintain a list of added items added.push(structure); // tell the structure that it has been added to a collection structure.emit('collection:add', this, options); } this.length = this.structures.length; if (added.length) this.emit('add', added); return this; } /** * Removes a structure from the collection. * * @param {Mixed} structures * @param {Object} options * @api public */ , remove: function remove(structures, options) { structures = Array.isArray(structures) ? structures.slice() : [structures]; options = options || {}; var removed = []; for (var i = 0, l = structures.length, structure; i < length; i++) { structure = this.get(structures[i]); if (!structure) continue; // remove it from the array this.structures.splice(this.structures.indexOf(structures), 1); delete this.structureId[structure.id]; this.length--; removed.push(structure); structure.emit('collection:remove', this, options); structure.collection = undefined; } this.emit('remove', removed); return this; } /** * Checks if the Collection has the given structure. * * @param {Mixed} obj * @param {Object} options * @returns {Boolean} * @api public */ , has: function has(obj, options) { var structures = options && options.plain ? this.plain() : this.structures; return !!~structures.indexOf(obj); } /** * Get a structure by id. * * @param {Mixed} id * @returns {Mixed} * @api public */ , get: function get(id) { return id ? this.structureId[id.id ? id.id : id] : null; } /** * Get a structure by index from structures. * * @param {Number} i * @returns {Mixed} * @api public */ , index: function index(i) { return i ? this.structures[i] : null; } /** * Get the last structure added to structures. * * @returns {Mixed} * @api public */ , last: function last() { return this.structures[this.length - 1]; } /** * Prepare a structure so it can be used in a Collection context, if we * don't receive a Structure instance we are going to generate a new one. * * @param {Mixed} structure * @param {Object} options * @api public */ , prepare: function prepare(structure, options) { options = options || {}; if (structure instanceof Structure) { structure.collection = this; } else { options.collection = this; structure = new this.structure(structure, options); } return structure; } /** * Parse the data that is added to the to collection * * @param {Array} obj * @api public */ , parse: function parse(data) { return data; } }); /** * Assign Cortex methods that we want to assign to our collection. * * @api private */ Cortex.forEach([ 'forEach', 'each' , 'map', 'collect' , 'filter', 'select' ], function forEach(method) { Collection.prototype[method] = function generated() { return Cortex[method].apply(this, [this.structures].concat(slice.call(arguments, 0))); }; }); /** * Cortex.View: * * A simple application view handler. * * @constructor * @param {Object} options * @api public */ var View = Cortex.View = function View(options) { options = options || {}; this.id = (uniqueId++) + ':' + (+new Date()); Cortex.extend(this, options); this.configure(); if (this.initialize) { this.initialize.apply(this, arguments); } }; Cortex.extend(View.prototype, EventEmitter, { /** * The default selector that we should create if we don't have a Node * reference. * * @type {String} * @api public */ selector: 'div' /** * Collection of HTML entities used to escape characters in a string. * * @type {Object} * @api public */ , entityMap: { '&': '&amp;' , '<': '&lt;' , '>': '&gt;' , '"': '&quot;' , "'": '&#39;' , '/': '&#x2F;' } /** * Event delegated. * * @type {Object} */ , delegate: {} /** * Find new elements * * @param {String} selector * @api public */ , $: function $(selector) { return this.$el.find(selector); } /** * Convenience method to escape output of the string. * * @param {String} string * @return {String} escaped string * @api public */ , escape: function escape(string) { var entityMap = this.entityMap; return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); } /** * Provides an alternate syntax for plates which provides you with * a chained syntax: * * this.template('name', data).where('href').has('/bar').insert('newurl'); * * @param {String} name name of the template file * @param {Object} data data for the template * @returns {Plates.Map} with an toString() method */ , template: function template(name, data) { // find the correct HTML template source, see if it's cached or if we // need to do a DOM lookup var html = name in template && template[name] ? template[name] : (template[name] = $('#plates-' + name).get('innerHTML')); // check if we received data, or default to a potential structure data = data || this.structure; if (data instanceof Structure || data instanceof Collection) { data = data.plain(); } // create a chainable syntax for the Plates template handler by adding // a .toString() method. This allows you to just spit it directly in to // HTML and it will render as string directly when concatenated with // other strings. var map = new Plates.Map({ create: true }); // by creating a toString method on the map, it allows us to pass the // map directly in to anything that expects a string and convert it // automatically to a proper template map.toString = function toString() { // prevent other kinds of invocations delete map.toString; return Plates.bind(html, data, this); }; return map; } /** * Configure the view and ensure that all elements and hooks are in place. * * @api private */ , configure: function configure() { // setup the internal element this.$el = $(this.selector); this.el = this.$el.plain(); // get the selectors of the view so we can make sure that all delegated // events are only trigged within this selector var roots = typeof this.selector === 'string' ? this.selector.split(/[\s+]?\,[\s+]?/) : []; // assign event delegation Cortex.forEach(roots, function rooting(root) { root = root ? root + ' ' : ''; Cortex.forEach(this.delegate, function delegate(handle, selector) { if (!this[handle] || typeof this[handle] !== 'function') { throw new Error('Undefined callback for delegated event ' + selector); } var query = selector.split(/^(\S+)\s*(.*)$/); // CSS selectors that are prefixed with a `< ` will not be prefixed with // the selector of the view and are allowed to be listen to the full // document selector = query[2].charAt(0) === '<' ? query[2].slice(2) : root + query[2]; Events.add(query[1], selector, this[handle], this); }, this); }, this); } }); /** * Self propagating extend function that Cortex uses. * * @param {Object} protoProps * @param {Object} classProps * @api public */ Structure.extend = Collection.extend = View.extend = function extend(protoProps, classProps) { var child = Cortex.inherits(this, protoProps, classProps); child.extend = this.extend; return child; }; /** * @copyright backbone.js * Helper function to correctly set up the prototype chain, for subclasses. * Similar to `goog.inherits`, but uses a hash of prototype properties and * class properties to be extended. */ function Ctor() {} Cortex.inherits = function inherits(parent, protoProps, staticProps) { var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent's constructor. if (protoProps && protoProps.hasOwnProperty('constructor')) { child = protoProps.constructor; } else { child = function child(){ parent.apply(this, arguments); }; } // Inherit class (static) properties from parent. Cortex.extend(child, parent); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. Ctor.prototype = parent.prototype; child.prototype = new Ctor(); // Add prototype properties (instance properties) to the subclass, // if supplied. if (protoProps) Cortex.extend(child.prototype, protoProps); // Add static properties to the constructor function, if supplied. if (staticProps) Cortex.extend(child, staticProps); // Correctly set child's `prototype.constructor`. child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed later. child.__super__ = parent.prototype; return child; }; /** * Cortex.Node: * * Cortex.Node is an DOM interface allows us to create a universal interface * between the DOM and our application code. This is where can add support for * different DOM operations such as a `css` method etc. * * @constructor * @api private */ var Node = Cortex.Node = Structure.extend({ initialize: function initialize(attributes) { this.attributes = typeof attributes === 'string' ? this.factory(attributes) : attributes; } /** * Get attributes attributes from the given Node instance. * * @param {String} attr * @returns {Mixed} * @api public */ , get: function get(attr) { var attributes = this.attributes , attribute; if (attr in attributes && attributes[attr]) return attributes[attr] || ''; attribute = attributes.getAttribute(attr); if (attribute) return attribute; // check for HTML5 dataset support or fallback to getAttribute hax if ('dataset' in attributes && typeof attributes.dataset === 'object' && attr in attributes.dataset) { return attributes.dataset[attr] || ''; } return attributes.getAttribute('data-' + attr) || ''; } /** * Set's new attributes on the Node instance, if an element doesn't * support it, it should should really be added as data-attribute. */ , set: function set(key, value, options) { var attributes; // special case for Node's if (typeof key === 'object' && 'nodeType' in key && key.nodeType === 1) { this.attributes = key; return this; } // support bulk updates, by allowing objects to set as a whole if (typeof key === 'object') { attributes = key; options = value; } else { attributes = {}; attributes[key] = value; } options = options || {}; if (!attributes) return this; if (attributes instanceof Structure) attributes = attributes.plain(); var current = this.attributes , cortex = this , changed = {} , added = {}; Cortex.forEach(attributes, function update(value, key) { if (options.unset) return cortex.remove(key); // determin if this a change or a addition var has = key in current , attr = current.hasAttribute('data-' + key) , currentvalue = cortex.get(key); if (has || attr && currentvalue !== value) changed[key] = true; if (!has && !attr) added[key] = true; // save the current version of the key in the history, so we can undo // changes if needed cortex.history[key] = currentvalue; if (has) { current[key] = value; } else { // we cannot set the data using the dataset property because it can // transform the key in to something completey different than you // added.. restoreId -> restore-id for example current.setAttribute('data-' + key, value); } }); Cortex.forEach(changed, function eachChange(value, key) { cortex.emit('changed:' + key, current[key], cortex.attributes[key]); cortex.emit('changed', key, current[key], cortex.attributes[key]); }); Cortex.forEach(added, function eachAdded(value, key) { cortex.emit('added:' + key, value); cortex.emit('added', key, value); }); this.attributes = current; return this; } /** * Generate an actual DOM element from a string. * * @param {String} dom * @returns {DOM} * @api public */ , factory: function factory(dom) { var div = document.createElement('div'); div.innerHTML = dom; return div.firstChild; } /** * Destroy the structure. * * @returns {Boolean} successful removement * @api public */ , destroy: function remove() { this.attributes.parentNode.removeChild(this.attributes); return true; } /** * This is where the syntax sugar starts. If you want to interact with the * DOM in any way this is the spot where you would want to be. * * Make sure that every single function that is specified here returns * `this` so we can chain different methods together. */ /** * Show the DOM element. * * @api public */ , show: function show() { this.attributes.style.display = this.attributes.style.display === 'none' ? '' : 'block'; return this; } /** * Hide the DOM element. * * @api public */ , hide: function hide() { this.attributes.style.display = 'none'; return this; } /** * Added classNames * * @param {String} className * @api public */ , addClass: function addClass(className) { var current = this.attributes.className , currentClasses = current.split(' ') , classes = className.split(' ') , i = classes.length; while (i--) { if (current && ~currentClasses.indexOf(classes[i])) continue; current += ' ' + classes[i]; } this.attributes.className = current; return this; } /** * remove className. * * @param {String} className * @api private */ , removeClass: function removeClass(className) { var current = this.attributes.className , classes = className.split(' ') , i = classes.length; while (i--) { current = current.replace( new RegExp('[\\s*]?\\b' + classes[i] + '\\b', 'g') , '' ); } this.attributes.className = current; return this; } }); /** * Cortex.Nodelist: * * A simple representation of a Node collection. * * @constructor * @api public */ var Nodelist = Cortex.Nodelist = Collection.extend({ /** * The default Structure that should be created. * * @type {Cortex.Node} */ structure: Cortex.Node /** * The selector that was used to generate this Nodelist * * @type {String} */ , selector: '' /** * Generate a new Nodelist instance with the matching Nodes. * * @param {String} selector * @returns {Cortext.Nodelist} a new Nodelist * @api public */ , find: function find(selector) { var nodes = []; // find all matching elements Cortex.forEach(this.plain(), function plainNodes(node) { var matches = $(selector, node); if (matches.length) Array.prototype.push.apply(nodes, matches.plain()); }); // now that we have all fetched all elements, return a new Nodelist // instance that holds all of these elements return new Nodelist(nodes, { selector: this.selector + ' ' + selector , wrapper: this }); } /** * End a chained .find() method by returning it's wrapping parent Nodelist * * @api public */ , end: function end() { return this.wrapper || this; } /** * Find the parent of the element. * * @param {String} selector * @returns {Cortex.NodeList} * @api public */ , parent: function parent(selector) { var nodes = [] , plain = this.plain(); if (selector) { Cortex.forEach(plain, function parents(node) { var parent = node.parentNode; while (parent) { if (Cortex.matchesSelector(selector, parent)) { return nodes.push(parent); } parent = parent.parentNode; } }); } else { // find all matching elements Cortex.forEach(plain, function plainNodes(node) { nodes.push(node.parentNode); }); } // now that we have all fetched all elements, return a new Nodelist // instance that holds all of these elements return new Nodelist(nodes, { selector: selector || this.selector , wrapper: this }); } }); /** * Generate proxy methods * * @TODO get/set/remove will probably override methods for the collection, so * we need to figure out a way around that. * @api private */ Cortex.forEach([ 'show', 'hide', 'addClass', 'removeClass', 'get', 'set', 'destroy' ], function generateProxy(method) { Nodelist.prototype[method] = function proxy() { var args = slice.call(arguments, 0) , chained = false , returns = []; this.forEach(function each(struct) { var returned = struct[method].apply(struct, args); // Detect if we are dealing with chained values here if (!chained) chained = returned === struct; // Make sure we return the values, even emptry strings or booleans if (returned !== undefined && returned !== struct) returns.push(returned); }); // Short hand for chained values if (chained) return this; if (!returns.length) return undefined; if (returns.length === 1) return returns[0]; return returns; }; }); /** * Cortex.Events: * * Cortex.Events takes care of all the DOM event handling. All events DOM * events eventually bubble down to the documentElement where they are * captured and checked against CSS selectors for events that are listening. */ var Events = Cortex.Events = { /** * Simple dictionary that contains all our listeners. * * @type {Object} * @api private */ listeners: {} /** * Add a new event listener. * * @param {String} type event type, click, dbclick, keydown etc. * @param {String} selectors CSS3 selector that matches the element * @param {Function} callback * @param {Function} context * @api public */ , add: function add(type, selector, callback, context) { // if we already have an emitter, start listening if (this.listeners[type]) return this.listeners[type].on(selector, callback, context); // setup a new EventEmitter instance this.listeners[type] = Cortex.extend({ type: type , listener: documentElement.addEventListener( type , this.process.bind(undefined, type) , true // needs to be set to true, or blur events won't work ) }, EventEmitter); // assign the event listener return this.listeners[type].on(selector, callback, context); } /** * Remove the listeners again. * * @param {String} type event type, click, dbclick, keydown etc. * @param {String} selectors CSS3 selector that matches the element * @param {Function} callback * @api public */ , remove: function remove(type, selector, callback) { if (!this.listeners[type]) return this; this.listeners[type].off(selector, callback); } /** * The function that processes events. * * @param {String} type event type * @param {Event} e DOM event * @api private */ , process: function process(type, e) { e = e || window.event; var element = e.target , listeners = Events.listeners[type]; // make sure that we still have listeners if (!type) return; // establish a list of possible dom matches by getting all the parent // elements of the clicked event var selectors = Object.keys(listeners.events) , nodes = [element] , node = element , matches = {} , parent; while (node = node.parentNode) { nodes.push(node); } // make sure that we get list of matching DOM nodes first before we // start emitting the events for the selectors as the called // EventEmitter can actually modify the state of the current DOM node // and make it pass for other selectors. Cortex.forEach(selectors, function forEach(selector) { for (var i = 0, l = nodes.length; i < l; i++) { if (Cortex.matchesSelector(selector, nodes[i])) { matches[selector] = nodes[i]; return; } } }); Cortex.forEach(matches, function forEachMatch(node, selector) { // We cannot re-set the correct target node as `e.target` as the // property is a `getter` only, and will throw errors in FireFox e.element = node; listeners.emit(selector, e); }); } }; /** * Cortex.push() allows us to load library files comply async, if Cortex does * not yet exist in the global scope it would be set to an array where the * functions will be pushed. So when this "main" library is loaded in, it will * search for all old Cortex instances and execute it. * * @param {String} name the name of the library * @param {Mixed} library either a string or a function. * @api public */ Cortex.push = function push(name, library) { if (name in Cortex.push) return this; Cortex.push[name] = typeof library === 'function' ? library(Cortex) : (new Function('Cortex', library))(Cortex); return this; }; /** * Simple CSS3 selector engine targeted for the high-end browsers. By using * our own interface it's relatively easy to hook in optional support for * different browsers. * * @param {String} selector * @param {DOM} context optional context * @returns {Cortex.Nodelist} * @api public */ Cortex.find = root.$ = function find(selector, context) { context = context || document; var nodes = [] , simple = /^[\#\.\w]?[\w\-]+$/.test(selector) , result , charAt; // simple selector optimization, because we are bad-ass like that if (typeof selector === 'string') { if (simple) { charAt = selector.charAt(0); // handle direct id lookups if (charAt === '#') { result = context.getElementById(selector.slice(1)); if (result) nodes.push(result); // className lookups } else if (charAt === '.') { nodes = slice.call(context.getElementsByClassName(selector.slice(1)), 0); // it would have been a regular tag name } else { nodes = slice.call(context.getElementsByTagName(selector), 0); } } else if ( selector.charAt(0) === '<' && selector.charAt(selector.length - 1) === '>' ) { nodes.push(selector); } else { nodes = Cortex.Sizzle(selector, context); } } else { nodes.push(selector); selector = selector.nodeName ? selector.nodeName.toLowerCase() : selector; } return new Nodelist(nodes, { selector: selector }); }; /** * Application framework bootstrapper. It scans the current document for * cerebral/cortex script types and parses them as JSON which will be used as * application configuration. * * @param {Function} fn the callback for when the cortex has become active * @api public */ Cortex.active = function active(fn) { if (Cortex.configured) { fn.call(Cortex, null, Cortex.cerebral); return this; } Cortex.active.on('loaded', function loaded() { // the initial configuration object var configuration = {}; Cortex.find('script[type="cerebral/cortex"]').forEach(function each(node) { var content = node.get('innerHTML'); if (!content) return; Cortex.extend( configuration // transform the content's of the script tag to plain JavaScript so we // can use it as a configuration object , (new Function('return ' + content.trim())()) ); }); Cortex.cerebral = Cortex.configuration = configuration; Cortex.configured = true; fn.call(Cortex, null, configuration); }); return this; }; // Make sure it inherits from EventEmitter so we can have it listen for loaded // events. Cortex.extend(Cortex.active, EventEmitter); /** * Create a new Cortex application once everything is loaded correctly. * * Options * - once: [name] defer initialization till Cortex.active.once is triggered * * @param {String} name name of the application * @param {Function} instance constructor of the application * @param {Object} options optional options for the app. * @api public */ Cortex.app = function app(name, Instance, options) { name = name.toLowerCase(); // are we setting or getting if (arguments.length === 1) return app[name]; if (app[name]) return this; function instantly () { Cortex.active(function (err, configuration) { if (app[name] || err) return; options = Cortex.extend(options || {}, configuration[name] || configuration); var instance = app[name] = new Instance(options); // notify the application that it has been loaded as a cortex app. if ('emit' in instance) instance.emit('cortex:app', options); }); } if (options && options.once) { Cortex.active.once(options.once, instantly); } else { instantly(); } return this; }; // EcmaScript methods used in this application, and should be polyfilled for // complete cross browser compatibility: // // - Object.keys() // - Array.indexOf() // - Array.filter() // - Array.map() // - Array.isArray() // - String.trim() // - Function.bind() /** * Importing third-party libraries so they are contained within our `Cortex` * namespace. The reqwest library likes to export to the global namespace * unless it detects a AMD define wrapper.. So we are going to emulate that * first. * * @param {String} name * @param {Function} fn * @api private */ function define(name, fn) { if (typeof name === 'function') { fn = name; name = define.importing; } Cortex[name] = fn(); } define.importing = 'undefined'; define.amd = true; /* [square] Directive: /home/swaagie/projects/cortex.js/node_modules/plates/lib/plates.js */ var Plates = (typeof module !== 'undefined' && 'id' in module && typeof exports !== 'undefined') ? exports : {}; !function(exports, env, undefined) { "use strict"; // // Cache variables to increase lookup speed. // var _toString = Object.prototype.toString; // // Polyfill the Array#indexOf method for cross browser compatibility. // [].indexOf || (Array.prototype.indexOf = function indexOf(a, b ,c){ for ( c = this.length , b = (c+ ~~b) % c; b < c && (!(b in this) || this[b] !==a ); b++ ); return b^c ? b : -1; }); // // Polyfill Array.isArray for cross browser compatibility. // Array.isArray || (Array.isArray = function isArray(a) { return _toString.call(a) === '[object Array]'; }); // // ### function fetch(data, mapping, value, key) // #### @data {Object} the data that we need to fetch a value from // #### @mapping {Object} The iterated mapping step // #### @tagbody {String} the tagbody we operated against // #### @key {String} optional key if the mapping doesn't have a dataKey // Fetches the correct piece of data // function fetch(data, mapping, value, tagbody, key) { key = mapping.dataKey || key; // // Check if we have data manipulation or filtering function. // if (mapping.dataKey && typeof mapping.dataKey === 'function') { return mapping.dataKey(data, value || '', tagbody || '', key); } // // See if we are using dot notation style // if (!~key.indexOf('.')) return data[key]; var result = key , structure = data; for (var paths = key.split('.'), i = 0, length = paths.length; i < length && structure; i++) { result = structure[+paths[i] || paths[i]]; structure = result; } return result !== undefined ? result : data[key]; } // // compileMappings // // sort the mappings so that mappings for the same attribute and value go consecutive // and inside those, those that change attributes appear first. // function compileMappings(oldMappings) { var mappings = oldMappings.slice(0); mappings.sort(function(map1, map2) { if (!map1.attribute) return 1; if (!map2.attribute) return -1; if (map1.attribute !== map2.attribute) { return map1.attribute < map2.attribute ? -1 : 1; } if (map1.value !== map2.value) { return map1.value < map2.value ? -1 : 1; } if (! ('replace' in map1) && ! ('replace' in map2)) { throw new Error('Conflicting mappings for attribute ' + map1.attribute + ' and value ' + map1.value); } if (map1.replace) { return 1; } return -1; }); return mappings; } // // Matches a closing tag to a open tag // function matchClosing(input, tagname, html) { var closeTag = '</' + tagname + '>',