UNPKG

straps

Version:

Class inheritance library with support for bean-style accessors.

348 lines (325 loc) 14.6 kB
/** * straps.js - Class inheritance library with support for bean-style accessors * * Copyright (c) 2006 - 2016 Juerg Lehni * http://scratchdisk.com/ * * Distributed under the MIT license. * * straps.js was created by extracting and simplifying the inheritance framework * from boostrap.js, a JavaScript DOM library, also created by Juerg Lehni: * https://github.com/lehni/bootstrap.js * * Inspirations: * http://dean.edwards.name/weblog/2006/03/base/ * http://dev.helma.org/Wiki/JavaScript+Inheritance+Sugar/ */ var Base = new function() { var hidden = /^(statics|enumerable|beans|preserve)$/, array = [], slice = array.slice, create = Object.create, describe = Object.getOwnPropertyDescriptor, define = Object.defineProperty, forEach = array.forEach || function(iter, bind) { // Poly-fill for forEach for (var i = 0, l = this.length; i < l; i++) { iter.call(bind, this[i], i, this); } }, forIn = function(iter, bind) { // Do not use Object.keys for iteration as iterators might modify // the object we're iterating over, making the hasOwnProperty still // necessary. for (var i in this) { if (this.hasOwnProperty(i)) iter.call(bind, this[i], i, this); } }, set = Object.assign || function(dst) { // Poly-fill for Object.assign for (var i = 1, l = arguments.length; i < l; i++) { var src = arguments[i]; for (var key in src) { if (src.hasOwnProperty(key)) dst[key] = src[key]; } } return dst; }, each = function(obj, iter, bind) { // Convention: Enumerate over the object using forEach if it defines // a value property named 'length' that contains an number value. // Everything else is enumerated using forIn. if (obj) { var desc = describe(obj, 'length'); (desc && typeof desc.value === 'number' ? forEach : forIn) .call(obj, iter, bind = bind || obj); } return bind; }; /** * Private function that injects functions from src into dest, overriding * the previous definition, preserving a link to it through Function#base. */ function inject(dest, src, enumerable, beans, preserve) { var beansNames = {}; /** * Private function that injects one field with given name and checks if * the field is a function with a previous definition that we need to * link to through Function#base. */ function field(name, val) { // This does even work for prop: 0, as it will just be looked up // again through describe. val = val || (val = describe(src, name)) && (val.get ? val : val.value); // Allow aliases to properties with different names, by having // string values starting with '#' if (typeof val === 'string' && val[0] === '#') val = dest[val.substring(1)] || val; var isFunc = typeof val === 'function', res = val, // Only lookup previous value if we preserve existing entries or // define a function that might need it for Function#base. If // a getter is defined, don't lookup previous value, but look if // the property exists (name in dest) and store result in prev. // Also check if the function doesn't already have #base defined // in which case it shall not be overridden. This occurs when // injecting statics from one constructor function to the next // for inheritance. e.g. in Mootools' Function#extend would // falsely be preserved up the inheritance chain through #base. prev = preserve || isFunc && !val.base ? (val && val.get ? name in dest : dest[name]) : null, bean; if (!preserve || !prev) { // Expose the 'super' function (meaning the one this function is // overriding) through Function#base: if (isFunc && prev) val.base = prev; // Produce bean properties if getters or setters are specified. // Just collect potential beans for now, and look them up in // dest at the end of fields injection. This ensures base works // for beans too, and inherits setters for redefined getters in // subclasses. if (isFunc && beans !== false && (bean = name.match(/^([gs]et|is)(([A-Z])(.*))$/))) beansNames[bean[3].toLowerCase() + bean[4]] = bean[2]; // No need to create accessor description if it already is one. // It is considered a description if it is a plain object with a // get function. if (!res || isFunc || !res.get || typeof res.get !== 'function' || !Base.isPlainObject(res)) { res = { value: res, writable: true }; } // Only set/change configurable and enumerable if this field is // configurable if ((describe(dest, name) || { configurable: true }).configurable) { res.configurable = true; // If no value is provided for enumerable, the default is to // allow any property to be enumerable except the functions // that create bean properties. res.enumerable = enumerable != null ? enumerable : !bean; } define(dest, name, res); } } // Iterate through all definitions in src now and call field() for each. if (src) { for (var name in src) { if (src.hasOwnProperty(name) && !hidden.test(name)) field(name); } // Now process the beans as well. for (var name in beansNames) { // Simple Beans Convention: // - If `beans: false` is specified, no beans are injected. // - `isName()` is only considered a getter of a bean accessor // if there is also a setter for it. // - If a potential getter has no parameters, it forms a bean // accessor. // - If `beans: true` is specified, the parameter count of a // potential getter is ignored and the bean is always created. var part = beansNames[name], set = dest['set' + part], get = dest['get' + part] || set && dest['is' + part]; if (get && (beans === true || get.length === 0)) field(name, { get: get, set: set }); } } return dest; } // The Base constructor function. function Base() { for (var i = 0, l = arguments.length; i < l; i++) { var src = arguments[i]; if (src) set(this, src); } return this; } // First inject static methods into Base function that is passed to inject() // and then returned as the Base class. return inject(Base, { inject: function(src/*, ... */) { if (src) { // Allow the whole scope to just define statics by defining // `statics: true` var statics = src.statics === true ? src : src.statics, beans = src.beans, preserve = src.preserve; if (statics !== src) inject(this.prototype, src, src.enumerable, beans, preserve); // Use the preserve setting in src.preserve for statics too, not // their own. inject(this, statics, null, beans, preserve); } // If there are more than one argument, loop through them and call // inject again. Do not simple inline the above code in one loop, // since each of the passed objects might override this.inject. for (var i = 1, l = arguments.length; i < l; i++) this.inject(arguments[i]); return this; }, extend: function(/* src, ... */) { var base = this, ctor, proto; // Look for an initialize function in all injection objects and use // it directly as the actual constructor. Also look for prototype, // in case a class wants to override it (e.g. Array inheritance). for (var i = 0, obj, l = arguments.length; i < l && !(ctor && proto); i++) { obj = arguments[i]; ctor = ctor || obj.initialize; proto = proto || obj.prototype; } // If no initialize function is provided, create a constructor that // simply calls the base constructor. ctor = ctor || function() { base.apply(this, arguments); }; proto = ctor.prototype = proto || create(this.prototype); // The new prototype extends the constructor on which extend is // called. Fix constructor. define(proto, 'constructor', { value: ctor, writable: true, configurable: true }); // Copy over static fields, as prototype-like inheritance // is not possible for static fields. inject(ctor, this); // Inject all the definitions in src. Use the new inject instead of // the one in ctor, in case it was overridden. this is needed when // overriding the static .inject(). But only inject if there's // something to actually inject. if (arguments.length) this.inject.apply(ctor, arguments); // Expose base property on constructor functions as well. // Do this after everything else, to avoid incorrect overriding of // `base` in inject() when creating long super call chains in // constructors. ctor.base = base; return ctor; } }).inject({ // Mark all properties in Base as not enumerable so that it can be used // as a basic hash-table class, e.g. `new Base(defaults, options); enumerable: false, /** * The Base constructor function. * * @param {...Object} objects one or multiple objects describing the * properties to set on the new created object. */ initialize: Base, /** * Sets properties on this object from one ore passed multiply objects. * * @param {...Object} objects one or multiple objects describing the * properties to set on this object. */ set: Base, /** * Injects the fields in this object from one ore passed multiply * objects. * * @param {...Object} objects one or multiple objects describing the * fields to be injected in this object. */ inject: function(/* src, ... */) { for (var i = 0, l = arguments.length; i < l; i++) { var src = arguments[i]; if (src) { inject(this, src, src.enumerable, src.beans, src.preserve); } } return this; }, /** * Returns a new object that inherits all properties from `this`, * through proper JS inheritance, without copying. * * Optionally, src parameters can be passed to inject into the newly * created object just like in {@link #inject()}, reflecting the * behavior of {@link Base.extend()}. * * @param {...Object} objects one or multiple objects describing the * fields to be injected in the newly created object. */ extend: function(/* src, ... */) { var res = create(this); return res.inject.apply(res, arguments); }, each: function(iter, bind) { return each(this, iter, bind); }, /** * General purpose clone function that delegates cloning to the * constructor that receives the object to be cloned as the first * argument. * NOTE: #clone() needs to be overridden in any class that requires * other cloning behavior. */ clone: function() { return new this.constructor(this); }, statics: { // Expose some local privates as static functions on Base. set: set, each: each, create: create, define: define, describe: describe, clone: function(obj) { return set(new obj.constructor(), obj); }, /** * Returns true if obj is a plain JavaScript object literal, or a * plain Base object, as produced by Base.merge(). */ isPlainObject: function(obj) { var ctor = obj != null && obj.constructor; // We also need to check for ctor.name === 'Object', in case // this is an object from another global scope (e.g. an iframe, // or another vm context in node.js). return ctor && (ctor === Object || ctor === Base || ctor.name === 'Object'); }, /** * Returns the 1st argument if it is defined, the 2nd otherwise. * `null` is counted as defined too, as !== undefined is used for * comparisons. */ pick: function(a, b) { return a !== undefined ? a : b; }, slice: function(list, begin, end) { return slice.call(list, begin, end); } } }); }; // Export Base class for node if (typeof module !== 'undefined') module.exports = Base;