roperzh/essential.js

View on GitHub
essential.js

Summary

Maintainability
F
6 days
Test Coverage
//     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;
}));