API Docs for: 0.0.1
Show:

File: src/js-ext/extra/classes.js

/**
 *
 * Pollyfils for often used functionality for Functions
 *
 * <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
 * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
 *
 * @module js-ext
 * @submodule extra/classes.js
 * @class Classes
 *
*/

require('polyfill/polyfill-base.js');
require('../lib/object.js');

(function (global) {

    "use strict";

    var NAME = '[Classes]: ',
        createHashMap = require('js-ext/extra/hashmap.js').createMap,
        DEFAULT_CHAIN_CONSTRUCT, defineProperty, defineProperties,
        NOOP, REPLACE_CLASS_METHODS, PROTECTED_CLASS_METHODS, PROTO_RESERVED_NAMES,
        BASE_MEMBERS, createBaseClass, Classes, coreMethods;

    global._ITSAmodules || Object.protectedProp(global, '_ITSAmodules', createHashMap());

/*jshint boss:true */
    if (Classes=global._ITSAmodules.Classes) {
/*jshint boss:false */
        module.exports = Classes; // Classes was already created
        return;
    }

    /**
     * Defines whether Classes should call their constructor in a chained way top-down.
     *
     * @property DEFAULT_CHAIN_CONSTRUCT
     * @default true
     * @type Boolean
     * @protected
     * @since 0.0.1
    */
    DEFAULT_CHAIN_CONSTRUCT = true;

    /**
     * Sugarmethod for Object.defineProperty creating an unenumerable property
     *
     * @method defineProperty
     * @param [object] {Object} The object to define the property to
     * @param [name] {String} name of the property
     * @param [method] {Any} value of the property
     * @param [force=false] {Boolean} to force assignment when the property already exists
     * @protected
     * @since 0.0.1
    */
    defineProperty = function (object, name, method, force) {
        if (!force && (name in object)) {
            return;
        }
        Object.defineProperty(object, name, {
            configurable: true,
            enumerable: false,
            writable: true,
            value: method
        });
    };
    /**
     * Sugarmethod for using defineProperty for multiple properties at once.
     *
     * @method defineProperties
     * @param [object] {Object} The object to define the property to
     * @param [map] {Object} object to be set
     * @param [force=false] {Boolean} to force assignment when the property already exists
     * @protected
     * @since 0.0.1
    */
    defineProperties = function (object, map, force) {
        var names = Object.keys(map),
            l = names.length,
            i = -1,
            name;
        while (++i < l) {
            name = names[i];
            defineProperty(object, name, map[name], force);
        }
    };

    /**
     * Empty function
     *
     * @method NOOP
     * @protected
     * @since 0.0.1
    */
    NOOP = function () {};

    /**
     * Internal hash containing the names of members which names should be transformed
     *
     * @property REPLACE_CLASS_METHODS
     * @default {destroy: '_destroy'}
     * @type Object
     * @protected
     * @since 0.0.1
    */
    REPLACE_CLASS_METHODS = createHashMap({
        destroy: '_destroy'
    });

    /**
     * Internal hash containing protected members: those who cannot be merged into a Class
     *
     *
     * @property PROTECTED_CLASS_METHODS
     * @default {$super: true, $superProp: true, $orig: true}
     * @type Object
     * @protected
     * @since 0.0.1
    */
    PROTECTED_CLASS_METHODS = createHashMap({
        $super: true,
        $superProp: true,
        $orig: true
    });

/*jshint proto:true */
/* jshint -W001 */
    /*
     * Internal hash containing protected members: those who cannot be merged into a Class
     *
     * @property PROTO_RESERVED_NAMES
     * @default {constructor: true, prototype: true, hasOwnProperty: true, isPrototypeOf: true,
     *           propertyIsEnumerable: true, __defineGetter__: true, __defineSetter__: true,
     *           __lookupGetter__: true, __lookupSetter__: true, __proto__: true}
     * @type Object
     * @protected
     * @since 0.0.1
    */
    PROTO_RESERVED_NAMES = createHashMap({
        constructor: true,
        prototype: true,
        hasOwnProperty: true,
        isPrototypeOf: true,
        propertyIsEnumerable: true,
        __defineGetter__: true,
        __defineSetter__: true,
        __lookupGetter__: true,
        __lookupSetter__: true,
        __proto__: true
    });
/* jshint +W001 */
/*jshint proto:false */

    defineProperties(Function.prototype, {

        /**
         * Merges the given prototypes of properties into the `prototype` of the Class.
         *
         * **Note1 ** to be used on instances --> ONLY on Classes
         * **Note2 ** properties with getters and/or unwritable will NOT be merged
         *
         * The members in the hash prototypes will become members with
         * instances of the merged class.
         *
         * By default, this method will not override existing prototype members,
         * unless the second argument `force` is true.
         *
         * @method mergePrototypes
         * @param prototypes {Object} Hash prototypes of properties to add to the prototype of this object
         * @param force {Boolean}  If true, existing members will be overwritten
         * @chainable
         */
        mergePrototypes: function (prototypes, force) {
            var instance, proto, names, l, i, replaceMap, protectedMap, name, nameInProto, finalName, propDescriptor, extraInfo;
            if (!prototypes) {
                return;
            }
            instance = this; // the Class
            proto = instance.prototype;
            names = Object.getOwnPropertyNames(prototypes);
            l = names.length;
            i = -1;
            replaceMap = arguments[2] || REPLACE_CLASS_METHODS; // hidden feature, used by itags

            protectedMap = arguments[3] || PROTECTED_CLASS_METHODS; // hidden feature, used by itags
            while (++i < l) {
                name = names[i];
                finalName = replaceMap[name] || name;
                nameInProto = (finalName in proto);
                if (!PROTO_RESERVED_NAMES[finalName] && !protectedMap[finalName] && (!nameInProto || force)) {
                    // if nameInProto: set the property, but also backup for chaining using $$orig
                    propDescriptor = Object.getOwnPropertyDescriptor(prototypes, name);
                    if (!propDescriptor.writable) {
                        console.info(NAME+'mergePrototypes will set property of '+name+' without its property-descriptor: for it is an unwritable property.');
                        proto[finalName] = prototypes[name];
                    }
                    else {
                        // adding prototypes[name] into $$orig:
                        instance.$$orig[finalName] || (instance.$$orig[finalName]=[]);
                        instance.$$orig[finalName][instance.$$orig[finalName].length] = prototypes[name];
                        if (typeof prototypes[name] === 'function') {
        /*jshint -W083 */
                            propDescriptor.value = (function (originalMethodName, finalMethodName) {
                                return function () {
        /*jshint +W083 */
                                    // this.$own = prot;
                                    // this.$origMethods = instance.$$orig[finalMethodName];
                                    var context, classCarierBkp, methodClassCarierBkp, origPropBkp, returnValue;
                                    // in some specific situations, this method is called without context.
                                    // can't figure out why (it happens when itable changes some of its its item-values)
                                    // probably reasson is that itable.model.items is not the same as itable.getData('_items')
                                    // anyway: to prevent errors here, we must return when there is no context:
                                    context = this;
                                    if (!context) {
                                        return;
                                    }
                                    classCarierBkp = context.__classCarier__;
                                    methodClassCarierBkp = context.__methodClassCarier__;
                                    origPropBkp = context.__origProp__;

                                    context.__methodClassCarier__ = instance;

                                    context.__classCarier__ = null;

                                    context.__origProp__ = finalMethodName;
                                    returnValue = prototypes[originalMethodName].apply(context, arguments);
                                    context.__origProp__ = origPropBkp;

                                    context.__classCarier__ = classCarierBkp;

                                    context.__methodClassCarier__ = methodClassCarierBkp;

                                    return returnValue;

                                };
                            })(name, finalName);
                        }
                        Object.defineProperty(proto, finalName, propDescriptor);
                    }
                }
                else {
                    extraInfo = '';
                    nameInProto && (extraInfo = 'property is already available (you might force it to be set)');
                    PROTO_RESERVED_NAMES[finalName] && (extraInfo = 'property is a protected property');
                    protectedMap[finalName] && (extraInfo = 'property is a private property');
                    console.warn(NAME+'mergePrototypes is not allowed to set the property: '+name+' --> '+extraInfo);
                }
            }
            return instance;
        },

        /**
         * Removes the specified prototypes from the Class.
         *
         *
         * @method removePrototypes
         * @param properties {String|Array} Hash of properties to be removed from the Class
         * @chainable
         */
        removePrototypes: function (properties) {
            var proto = this.prototype,
                replaceMap = arguments[1] || REPLACE_CLASS_METHODS; // hidden feature, used by itags
            Array.isArray(properties) || (properties=[properties]);
            properties.forEach(function(prop) {
                prop = replaceMap[prop] || prop;
                delete proto[prop];
            });
            return this;
        },

        /**
         * Redefines the constructor fo the Class
         *
         * @method setConstructor
         * @param [constructorFn] {Function} The function that will serve as the new constructor for the class.
         *        If `undefined` defaults to `NOOP`
         * @param [prototypes] {Object} Hash map of properties to be added to the prototype of the new class.
         * @param [chainConstruct=true] {Boolean} Whether -during instance creation- to automaticly construct in the complete hierarchy with the given constructor arguments.
         * @chainable
         */
        setConstructor: function(constructorFn, chainConstruct) {
            var instance = this;
            if (typeof constructorFn==='boolean') {
                chainConstruct = constructorFn;
                constructorFn = null;
            }
            (typeof chainConstruct === 'boolean') || (chainConstruct=DEFAULT_CHAIN_CONSTRUCT);
            instance.$$constrFn = constructorFn || NOOP;
            instance.$$chainConstructed = chainConstruct ? true : false;
            return instance;
        },

        /**
         * Returns a newly created class inheriting from this class
         * using the given `constructor` with the
         * prototypes listed in `prototypes` merged in.
         *
         *
         * The newly created class has the `$$super` static property
         * available to access all of is ancestor's instance methods.
         *
         * Further methods can be added via the [mergePrototypes](#method_mergePrototypes).
         *
         * @example
         *
         *  var Circle = Shape.subClass(
         *      function (x, y, r) {
         *          // arguments will automaticly be passed through to Shape's constructor
         *          this.r = r;
         *      },
         *      {
         *          area: function () {
         *              return this.r * this.r * Math.PI;
         *          }
         *      }
         *  );
         *
         * @method subClass
         * @param [constructor] {Function} The function that will serve as constructor for the new class.
         *        If `undefined` defaults to `NOOP`
         * @param [prototypes] {Object} Hash map of properties to be added to the prototype of the new class.
         * @param [chainConstruct=true] {Boolean} Whether -during instance creation- to automaticly construct in the complete hierarchy with the given constructor arguments.
         * @return the new class.
         */
        subClass: function (constructor, prototypes, chainConstruct) {

            var instance = this,
                constructorClosure = {},
                baseProt, proto, constrFn;
            if (typeof constructor === 'boolean') {
                constructor = null;
                prototypes = null;
                chainConstruct = constructor;
            }

            else {
                if (Object.isObject(constructor)) {
                    chainConstruct = prototypes;
                    prototypes = constructor;
                    constructor = null;
                }

                if (typeof prototypes === 'boolean') {
                    chainConstruct = prototypes;
                    prototypes = null;
                }
            }

            (typeof chainConstruct === 'boolean') || (chainConstruct=DEFAULT_CHAIN_CONSTRUCT);

            constrFn = constructor || NOOP;
            constructor = function() {
                constructorClosure.constructor.$$constrFn.apply(this, arguments);
            };

            constructor = (function(originalConstructor) {
                return function() {
                    var context = this;
                    if (constructorClosure.constructor.$$chainConstructed) {
                        context.__classCarier__ = constructorClosure.constructor.$$super.constructor;
                        context.__origProp__ = 'constructor';
                        context.__classCarier__.apply(context, arguments);
                        context.$origMethods = constructorClosure.constructor.$$orig.constructor;
                    }
                    context.__classCarier__ = constructorClosure.constructor;
                    context.__origProp__ = 'constructor';
                    originalConstructor.apply(context, arguments);
                    // only call aferInit on the last constructor of the chain:
                    (constructorClosure.constructor===context.constructor) && context.afterInit();
                };
            })(constructor);

            baseProt = instance.prototype;
            proto = Object.create(baseProt);
            constructor.prototype = proto;

            // webkit doesn't let all objects to have their constructor redefined
            // when directly assigned. Using `defineProperty will work:
            Object.defineProperty(proto, 'constructor', {value: constructor});

            constructor.$$chainConstructed = chainConstruct ? true : false;
            constructor.$$super = baseProt;
            constructor.$$orig = {
                constructor: constructor
            };
            constructor.$$constrFn = constrFn;
            constructorClosure.constructor = constructor;
            prototypes && constructor.mergePrototypes(prototypes, true);
            return constructor;
        }

    });

    global._ITSAmodules.Classes = Classes = {};

    /**
     * Base properties for every Class
     *
     *
     * @property BASE_MEMBERS
     * @type Object
     * @protected
     * @since 0.0.1
    */
    BASE_MEMBERS = {
       /**
        * Transformed from `destroy` --> when `destroy` gets invoked, the instance will invoke `_destroy` through the whole chain.
        * Defaults to `NOOP`, so that it can be always be invoked.
        *
        * @method _destroy
        * @private
        * @chainable
        * @since 0.0.1
        */
        _destroy: NOOP,

       /**
        * Transformed from `destroy` --> when `destroy` gets invoked, the instance will invoke `_destroy` through the whole chain.
        * Defaults to `NOOP`, so that it can be always be invoked.
        *
        * @method afterInit
        * @private
        * @chainable
        * @since 0.0.1
        */
        afterInit: NOOP,

       /**
        * Calls `_destroy` on through the class-chain on every level (bottom-up).
        * _destroy gets defined when the itag defines `destroy` --> transformation under the hood.
        *
        * @method destroy
        * @param [notChained=false] {Boolean} set this `true` to prevent calling `destroy` up through the chain
        * @chainable
        * @since 0.0.1
        */
        destroy: function(notChained) {
            var instance = this,
                superDestroy;
            if (!instance._destroyed) {
                superDestroy = function(constructor) {
                    // don't call `hasOwnProperty` directly on obj --> it might have been overruled
                    Object.prototype.hasOwnProperty.call(constructor.prototype, '_destroy') && constructor.prototype._destroy.call(instance);
                    if (!notChained && constructor.$$super) {
                        instance.__classCarier__ = constructor.$$super.constructor;
                        superDestroy(constructor.$$super.constructor);
                    }
                };
                // instance.detachAll();  <-- is what Event will add
                // instance.undefAllEvents();  <-- is what Event will add
                superDestroy(instance.constructor);
                Object.protectedProp(instance, '_destroyed', true);
            }
            return instance;
        }
    };

    coreMethods = Classes.coreMethods = {
        /**
         * Returns the instance, yet sets an internal flag to a higher Class (1 level up)
         *
         * @property $super
         * @chainable
         * @for BaseClass
         * @since 0.0.1
        */
        $super: {
            get: function() {
                var instance = this;
                instance.__classCarier__ || (instance.__classCarier__= instance.__methodClassCarier__);
                instance.__$superCarierStart__ || (instance.__$superCarierStart__=instance.__classCarier__);
                instance.__classCarier__ = instance.__classCarier__ && instance.__classCarier__.$$super.constructor;
                return instance;
            }
        },

        /**
         * Calculated value of the specified member at the parent-Class.
         *
         * @method $superProp
         * @return {Any}
         * @since 0.0.1
        */
        $superProp: {
            configurable: true,
            writable: true,
            value: function(/* member, *args */) {
                var instance = this,
                    classCarierReturn = instance.__$superCarierStart__ || instance.__classCarier__ || instance.__methodClassCarier__,
                    currentClassCarier = instance.__classCarier__ || instance.__methodClassCarier__,
                    args = arguments,
                    superClass, superPrototype, firstArg, returnValue;

                instance.__$superCarierStart__ = null;
                if (args.length === 0) {
                    instance.__classCarier__ = classCarierReturn;
                    return;
                }

                superClass = currentClassCarier.$$super.constructor,
                superPrototype = superClass.prototype,
                firstArg = Array.prototype.shift.apply(args); // will decrease the length of args with one
                if ((firstArg==='constructor') && currentClassCarier.$$chainConstructed) {
                    console.warn('the constructor of this Class cannot be invoked manually, because it is chainConstructed');
                    return currentClassCarier;
                }
                if (typeof superPrototype[firstArg] === 'function') {
                    instance.__classCarier__ = superClass;
                    returnValue = superPrototype[firstArg].apply(instance, args);
                }
                instance.__classCarier__ = classCarierReturn;
                return (returnValue!==undefined) ? returnValue : superPrototype[firstArg];
            }
        },

        /**
         * Invokes the original method (from inside where $orig is invoked).
         * Any arguments will be passed through to the original method.
         *
         * @method $orig
         * @return {Any}
         * @since 0.0.1
        */
        $orig: {
            configurable: true,
            writable: true,
            value: function() {
                var instance = this,
                    classCarierReturn = instance.__$superCarierStart__,
                    currentClassCarier = instance.__classCarier__ || instance.__methodClassCarier__,
                    args = arguments,
                    propertyName = instance.__origProp__,
                    returnValue, origArray, orig, item;

                instance.__$superCarierStart__ = null;

                origArray = currentClassCarier.$$orig[propertyName];

                instance.__origPos__ || (instance.__origPos__ = []);

                // every class can have its own overruled $orig for even the same method
                // first: seek for the item that matches propertyName/classRef:
                instance.__origPos__.some(function(element) {
                    if ((element.propertyName===propertyName) && (element.classRef===currentClassCarier)) {
                        item = element;
                    }
                    return item;
                });

                if (!item) {
                    item = {
                        propertyName: propertyName,
                        classRef: currentClassCarier,
                        position: origArray.length-1
                    };
                    instance.__origPos__.push(item);
                }
                if (item.position===0) {
                    return undefined;
                }
                item.position--;
                orig = origArray[item.position];
                if (typeof orig === 'function') {
                    instance.__classCarier__ = currentClassCarier;
                    returnValue = orig.apply(instance, args);
                }
                instance.__classCarier__ = classCarierReturn;

                item.position++;

                return (returnValue!==undefined) ? returnValue : orig;
            }
        }
    };

   /**
    * Creates the base Class: the highest Class in the hierarchy of all Classes.
    * Will get extra properties merge into its prototype, which leads into the formation of `BaseClass`.
    *
    * @method createBaseClass
    * @protected
    * @return {Class}
    * @for Classes
    * @since 0.0.1
    */
    createBaseClass = function () {
        var InitClass = function() {};
        return Function.prototype.subClass.apply(InitClass, arguments);
    };

    /**
     * The base BaseClass: the highest Class in the hierarchy of all Classes.
     *
     * @property BaseClass
     * @type Class
     * @since 0.0.1
    */
    Object.protectedProp(Classes, 'BaseClass', createBaseClass().mergePrototypes(BASE_MEMBERS, true, {}, {}));

    // because `mergePrototypes` cannot merge object-getters, we will add the getter `$super` manually:
    Object.defineProperties(Classes.BaseClass.prototype, coreMethods);

    /**
     * Returns a base class with the given constructor and prototype methods
     *
     * @method createClass
     * @param [constructor] {Function} constructor for the class
     * @param [prototype] {Object} Hash map of prototype members of the new class
     * @return {Class} the new class
    */
    Object.protectedProp(Classes, 'createClass', Classes.BaseClass.subClass.bind(Classes.BaseClass));

    module.exports = Classes;

}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this));