Skip to content

Instantly share code, notes, and snippets.

@Skateside
Created October 8, 2012 20:26
Show Gist options
  • Save Skateside/3854762 to your computer and use it in GitHub Desktop.
Save Skateside/3854762 to your computer and use it in GitHub Desktop.
Started using mixins quite a bit so I built a few functions to make it easier.
// These shims patch methods that the SK80 micro-library uses. References to the
// ES5 spec have been included to ensure that the shims are as close to
// standards as possible.
(function () {
'use strict';
var isStringArray = 'a'[0] === 'a',
objProto = Object.prototype,
toString = objProto.toString;
// http://es5.github.com/#x9.9
function toObject(o) {
if (o === undefined || o === null) {
throw {
name: 'TypeError',
message: o + ' is null or not an object'
};
}
if (!isStringArray && toString.call(o) === '[object String]') {
o = o.split('');
}
return Object(o);
}
// http://es5.github.com/#sign
function sign(number) {
return number < 0 ? -1 : 1;
}
// Used a few times in the specs, always described like this:
// sign(number) * floor(abs(number))
function makeInt(n) {
return sign(n) * Math.floor(Math.abs(n));
}
// http://es5.github.com/#x9.5
function toUint32(n) {
var number = +n,
ret = 0,
twoPow32 = Math.pow(2, 32);
if (!isNaN(number) && isFinite(number)) {
ret = makeInt(number) % twoPow32;
if (ret >= twoPow32 / 2) {
ret -= twoPow32;
}
}
return ret;
}
// http://es5.github.com/#x9.4
function toInteger(str) {
var number = Number(str),
returnValue = number;
if (isNaN(number)) {
returnValue = 0;
} else if (number !== 0 && isFinite(number)) {
returnValue = makeInt(number);
}
return returnValue;
}
// http://es5.github.com/#x15.4.4.18
if (!Array.prototype.hasOwnProperty('forEach')) {
Array.prototype.forEach = function (func, thisArg) {
var index = 0,
array = toObject(this),
length = toUint32(array.length);
if (toString.call(func) !== '[object Function]') {
throw {
name: 'TypeError',
message: func + ' is not a function'
};
}
while (index < length) {
if (objProto.hasOwnProperty.call(array, index)) {
func.call(thisArg, array[index], index, array);
}
index += 1;
}
};
}
// http://es5.github.com/#x15.4.4.14
if (!Array.prototype.hasOwnProperty('indexOf')) {
Array.prototype.indexOf = function (search, offset) {
var array = toObject(this),
len = toUint32(array.length),
n = offset === undefined ? 0 : toInteger(offset),
index = -1;
if (len > 0 && len > n) {
if (n < 0) {
n = Math.max(0, len - Math.abs(n));
}
while (n < len) {
if (objProto.hasOwnProperty.call(array, index) &&
array[n] === search) {
index = n;
break;
}
n += 1;
}
}
return index;
};
}
// http://es5.github.com/#x15.4.3.2
if (!Array.hasOwnProperty('isArray')) {
Array.isArray = function (o) {
return toString.call(o) === '[object Array]';
};
}
// http://es5.github.com/#x15.2.3.5
if (!Object.hasOwnProperty('create')) {
Object.create = function (proto, properties) {
var object,
F = function () {};
if (proto === null) {
object = {'__proto__': null};
} else {
if (typeof proto !== 'object') {
throw {
name: 'TypeError',
message: 'typeof prototype[' + (typeof proto) + '] ' +
'must be an object'
};
}
F.prototype = proto;
object = new F();
object.__proto__ = proto;
}
if (properties !== undefined) {
throw {
name: 'BrowserError',
message: 'The second arguments of Object.create() is not' +
'supported in this browser'
};
}
return object;
};
}
// http://es5.github.com/#x15.2.3.2
if (!Object.hasOwnProperty('getPrototypeOf')) {
Object.getPrototypeOf = function (object) {
return object.__proto__ || (
object.constructor ?
object.constructor.prototpe :
Object.prototype
);
};
}
}());
// The SK80 mixin. Adds 2 properties and 2 methods to an object.
//
// Properties:
// this.mixins (Object) A public store for the mixins. Initially
// empty.
// this.inter (Object) An object to create and check interfaces.
// this.inter.init() Defines an interface.
// this.inter.matches() Checks that an object matches the
// interface.
// Methods:
// this.addMixin() Adds a mixin to the public store.
// this.create() Creates an object based on another. Can also
// fire the objects "init" method and add
// mixins.
//
// It can be used in any of three ways.
//
// To add these properties to your own namespace:
// SK80.call(MYNAMESPACE);
// To contain these properties within the SK80 namespace:
// SK80.call(SK80);
// To create a new object with these properties:
// var myObject = new SK80();
//
// The SK80.namespace has another property "version" which contains information
// about the current version of this micro-library. This property is not added
// to another namespace.
var SK80 = (function () {
'use strict';
var sk80,
version = '0.3b',
toString = Object.prototype.toString,
reserved = ['arguments', 'break', 'case', 'catch', 'class', 'const',
'continue', 'debugger', 'default', 'do', 'else', 'enum', 'extends',
'false', 'finally', 'for', 'function', 'if', 'implements', 'import',
'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package',
'private', 'protected', 'public', 'return', 'super', 'static',
'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void',
'while', 'with', 'yield'];
// Checks to ensure that a given object is a String.
//
// Takes:
// object (Mixed) The object to check.
// Returns:
// (Boolean) true if a String, false otherwise.
function isString(object) {
return toString.call(object) === '[object String]';
}
// Checks to ensure that a given object is a Function.
//
// Takes:
// object (Mixed) The object to check.
// Returns:
// (Boolean) true if a Function, false otherwise.
function isFunction(object) {
return toString.call(object) === '[object Function]';
}
// Works out the type of a given object, based on the object's [[Class]]. The
// function fixes a few browser quirks with undefined and null, it also reduces
// the [[Class]] of all DOM nodes to "htmlelement". Returned strings are always
// lowercase.
//
// Takes:
// object (Object) The object to analyse.
// Returns:
// (String) The [[Class]] of the object, always lower case
// and DOM nodes are always "htmlelement".
function getClass(object) {
var string;
if (object === undefined) {
string = 'undefined';
} else if (object === null) {
string = 'null';
} else if (getClass(object.nodeName) === 'string'
&& getClass(object.nodeType) === 'number') {
string = 'htmlelement';
} else {
string = toString.call(object);
string = string.substr(8);
string = string.substr(0, string.length - 1).toLowerCase();
}
return string;
}
// Checks that a property exists somewhere in the prototype chain of a given
// object and that the value is undefined. This function exists because
// hasOwnProperty only works on the own properties, not the inherited ones and
// to simply check that o[prop] === undefined will return true if prop doesn't
// exist. We need to know that it does and has the value undefined.
//
// Takes:
// object (Object) The object to check for the property.
// prop (String) The property name to check.
// Returns:
// (Boolean) true if the property exists, false otherwise.
function checkUndefined(object, prop) {
var proto = object,
prev = object,
hasProp = false;
do {
// Walk up the prototype chain. To prevent an infinite loop from occuring if
// object is an HTMLElement, break the loop if proto and prev are the same.
proto = Object.getPrototypeOf(prev);
if (proto === prev || proto === null) {
break;
}
if (proto.hasOwnProperty(prop) && proto[prop] === undefined) {
hasProp = true;
break;
}
prev = proto;
} while (proto);
return hasProp;
}
sk80 = function () {
// The mixins Object contains all the mixins created for this application. It's
// public so that the mixins may be accessed directly.
this.mixins = {};
// Adds mixins to the mixins Object and performs type checking and ensures that
// an identically named mixin does not already exist. Also checks that the mixin
// name is not a reserved work in JavaScript.
//
// Takes:
// name (String) The name of the mixin. It should be a valid
// Object key.
// mixin (Function) The mixin function. The "this" keyword will
// point to the Object that this mixin is added to.
this.addMixin = function (name, mixin) {
if (!isString(name)) {
throw {
name: 'TypeError',
message: 'SK80.addMixin name argument must be a String'
};
}
if (!isFunction(mixin)) {
throw {
name: 'TypeError',
message: 'SK80.addMixin mixin argument must be a Function'
};
}
if (this.mixins.hasOwnProperty(name)) {
throw {
name: 'SyntaxError',
message: 'SK80.addMixin "' + name + '" mixin has already ' +
'been defined'
};
}
if (reserved.indexOf(name) > -1) {
throw {
name: 'SyntaxError',
message: 'SK80.addMixin "' + name + '" is a reserved ' +
'word in JavaScript'
};
}
this.mixins[name] = mixin;
};
// Creates objects based on the arguments that are given. The object argument is
// the Object from which the new object should be created, the optional settings
// arguments helps define the newly created object.
//
// Takes:
// object (Object) The object from which to create the new
// object.
// [settings] (Object) Settings for the newly created object. Has
// two possible keys:
// mixins (Array) An Array of Strings, the names of the mixins
// to add to the newly created object.
// args (Array) Any arguments to pass to the newly created
// objects "init" method. If the object has no
// "init" method, no action is taken.
// Returns:
// (Object) The newly created object.
this.create = function (object, settings) {
var store = this.mixins,
created = Object.create(object);
if (settings !== undefined) {
if (settings.hasOwnProperty('mixins')) {
if (!Array.isArray(settings.mixins)) {
throw {
name: 'TypeError',
message: 'SK80.create mixins must be an Array'
};
}
settings.mixins.forEach(function (mixin) {
if (!isString(mixin)) {
throw {
name: 'TypeError',
message: 'SK80.create settings.mixins must ' +
'be an Array of Strings'
};
}
if (!store.hasOwnProperty(mixin)) {
throw {
name: 'ReferenceError',
message: 'SK80.create "' + mixin + '" mixin ' +
'cannot be found'
};
}
if (!isFunction(store[mixin])) {
throw {
name: 'TypeError',
message: 'SK80.create "' + mixin + '" mixin ' +
'is not a Function'
};
}
store[mixin].call(created);
});
}
if (settings.hasOwnProperty('args')
&& isFunction(created.init)) {
if (!Array.isArray(settings.args)) {
throw {
name: 'TypeError',
message: 'SK80.create settings.args must be an ' +
'Array'
};
}
created.init.apply(created, settings.args);
}
}
return created;
};
// Allows us to define interfaces. The given object must contain the stored
// methods; Errors are thrown if it does not match. In JavaScript, "interface"
// is a reserved word, so we use "inter" here instead.
this.inter = {
// Creates the interface by defining a name and the methods it requires.
//
// Takes:
// name (String) The name of the interface.
// properties (Object) The properties of the interface and their
// types in key/value pairs.
init: function (name, properties) {
var prop;
if (!isString(name)) {
throw {
name: 'TypeError',
message: 'SK80.inter name arguments must be a String'
};
}
this.name = name;
for (prop in properties) {
if (properties.hasOwnProperty(prop)) {
if (!isString(prop)) {
throw {
name: 'TypeError',
message: 'SK80.inter properties argument can ' +
'only contain Strings, ' + (typeof prop) +
' given'
};
}
properties[prop] = properties[prop].toLowerCase();
}
}
this.properties = properties;
},
// Checks to ensure that a given object implements this interface. This function
// throws Errors if a mis-match is detected. Since "implements" is a reserved
// word in JavaScript, we use "matches" instead.
//
// Takes:
// object (Object) The object that should implement this interface.
// Returns:
// (Boolean) true if the object implements this interface.
matches: function (object) {
var prop,
gotten,
current;
if (!this.name || !this.properties) {
throw {
name: 'InitError',
message: 'SK80.inter.matches called on a non-' +
'initialised interface'
};
}
for (prop in this.properties) {
if (this.properties.hasOwnProperty(prop)) {
gotten = getClass(object[prop]);
current = this.properties[prop];
if (current === 'undefined' &&
!checkUndefined(object, prop)) {
gotten = 'missing';
}
if (gotten !== current) {
throw {
name: 'ValidationError',
message: 'Object does not match the "' +
this.name + '" interface: "' + prop + '" ' +
'property is ' + gotten + ', should be ' +
current
};
}
}
}
return true;
}
};
};
sk80.version = version;
return sk80;
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment