storkCore

Artifact [c4930f07b4]
Login

Artifact [c4930f07b4]

Artifact c4930f07b4348118ae97be9565185664b09844c6:


/*
 * Set of routines and components to easily built dynamic, functional UIs
 * in Javascript.
 * It somewhat models itself after two concepts:
 *
 *  - Classic MVC patterns with controllers, models and views.
 *  - Self-style prototype object orientation by cloning and no classes
 *
 * As Javascript is a prototype object language a more Self-like object
 * model is suitable, and provides certain benefits over the glued on
 * class-based models often seen with Javascript. The main notion is that
 * any objects can be cloned to create new ones, with the clones inheriting
 * from their original object. One clear benefit is that the original
 * objects are real prototypes or templates: they can be used as-is,
 * thus offering neat testing and prototyping.
 *
 * Author: Kristoffer Lawson, setok@setok.co, 2010-2022
 *
 */


/**
 * @property {Number} includedNum
 *      unused?
 */
var includedNum = 0;

function getPrototypeOf(object) {
    var proto = undefined;
    if (typeof Object.getPrototypeOf !== "function") {
        if (typeof "test".__proto__ === "object") {
            proto = object.__proto__;
        }
        else {
            proto = object.constructor.prototype;
        }
    }
    else {
        proto = Object.getPrototypeOf(object);
    }
    return proto;
};

/**
 *
 * A clone of an object is an empty object
 * with a prototype reference to the original.
 *
 * For StorkCore we're using an OO model like Self, where there are no
 * classes as such, but objects can clone from any other objects (and
 * inherit from them). Self is the definitive prototype object language and
 * JavaScript is actually based on that notion, which is effective for
 * GUIs. Thus the simple model actually works well with JS.
 *
 * Each cloned object will have a superMethod() available to them, which
 * is a kind of super(). It allows them to call any method from their parent
 * upwards, but with this = object. Can be used when overriding methods to
 * call the original. superMethod should usually be called with a reference
 * to the current method (the one that extended the original), and the
 * string name of the method. E.g. if, within Employee.init() you wanted
 * to do a super to Person.init(), superMethod would be called:
 *
 *     superMethod(Employee.init, "init");
 *
 * This is awkward, but Javascript does not appear to offer a more
 * beautiful alternative.
 *
 * @param {Object} obj
 *      the object to be cloned
 * @return {Object}
 *      the cloned object
 */
function clone(obj) {
    if ( (obj === undefined) || (obj === null) ) {
        return obj;
    }

    // a private constructor, used only by this one clone.
    function Clone() {
        this._base = obj;
        //console.info("object: %s", obj);
    }

    Clone.prototype = obj;
    var newOb = new Clone();
    if (newOb.cloned != undefined) {
        newOb.cloned();
    }

    /*
     * Goes up the inheritance chain to call a method from this object's
     * ancestors.
     *
     * @param {Object} caller
     *      the method calling this method (used to set the scope)
     * @param {Function} methodName
     *      the method to call.
     * @param {Object...} args
     *      variable number of arguments can be passed to superMethod(). These
     *      will be passed to the final method as normal arguments.
     *
     * @return the return value of super method.
     */
    newOb.superMethod = function(caller, methodName, args) {
        var ob = this;
        var callingImplementation = undefined;
        var max = 50;
        var counter = 0;
        while(callingImplementation == undefined) {
            if(!ob) {
                return;
            }
            if(counter === max) {
                logError("superMethod: Couldn't find the calling implementation.", caller);
                return;
            }

            if(ob.hasOwnProperty(methodName) &&
               ob[methodName] === caller) {
                callingImplementation = ob;
            }

            ob = getPrototypeOf(ob);
            counter++;
        }

        if(!ob) {
            return;
        }

        args = Array.prototype.slice.call(arguments, 2);
        return ob[methodName].apply(this, args);
    };

    return newOb;
}


// adding bind to the prototype of Functions, falling back to native support if needed
if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5 internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                                 ? this
                                 : oThis,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}


var cssSupports = (function() {
    var div = document.createElement('div');
    var vendors = 'Khtml Ms O Moz Webkit'.split(' ');

    return function(prop) {
        var len = vendors.length;
        if ( prop in div.style ) return true;

        prop = prop.replace(/^[a-z]/, function(val) {
            return val.toUpperCase();
        });

        while(len--) {
            if ( vendors[len] + prop in div.style ) {
                return true;
            }
        }
        return false;
    };
})();

function logError(error) {
    if(window.console && window.console.error) {
        window.console.error(error);
    }
};


/**
 * Return true if an element with given 'id' exists in the document DOM.
 */

function elementExists(id) {
    return document.getElementById(id);
};


/**
 * Similar to document.getElementByID, but throws an error if not found.
 */

function elementByID(id) {
    var element = document.getElementById(id);
    if (element == undefined) {
        throw "Element '" + id + "' could not be found";
    } else {
        return element;
    }
};


/**
 * Search through element and its children to find the element with 'id' and
 * return it.
 */

function containedElementByID(element, id) {
    var i, r;

    //    console.log("containedElementByID");
    //    console.log("containedElementByID - element.id: " + element.id);
    if (element.id == id) {
        return element;
    } else {
        for (i=0; i < element.children.length; i++) {
            r = containedElementByID(element.children[i], id);
            if (r != null) {
                return r;
            }
            //return containedElementByID(element.children[i], id);
        }
        return null;
    }
}

function getText(element) {
    if(isString(element)) {
        element = elementByID(element);
    }
    var text = undefined;
    if(element.textContent) {
        text = element.textContent;
    }
    return text;
}

/**
 * Set the textual content of HTML element
 */

function setTextOfElement(element, text) {
    if(isString(element)) {
        element = elementByID(element);
    }
    emptyElement(element);
    element.appendChild(document.createTextNode(text));
}


/**
 * Set the textual content of element defined by 'id'.
 * Throws error if 'id' was not found in the document.
 */

function setTextOfElementID(id, text) {
    setTextOfElement(id, text);
}

/**
 * Number?
 */

function isNumber(object) {
    return Object.prototype.toString.call(object) == '[object Number]';
}

/**
 * String?
 */

function isString(object) {
    return Object.prototype.toString.call(object) == '[object String]';
}

function trim(string) {
    return string ? string.replace(/^\s+|\s+$/g, "") : "";
}

/**
 * Array?
 */

function isArray(object) {
    return Object.prototype.toString.call(object) == '[object Array]';
}

/**
 * Element?
 * @param {Object} object
 * @return {Boolean} if element, true otherwise false
 */

function isElement(object) {
    return (object && object.nodeType == 1);
}

/**
 * Function?
 * @param {Object} object
 * @return {Boolean} if element, true otherwise false
 */

function isFunction(object) {
    return Object.prototype.toString.call(object) === '[object Function]';
};

/** Checks to see if 'element' has a 'className'. **/

function hasClass(element, className) {
    if(isString(element)) {
        element = elementByID(element);
    }
    if (element && element.className && className) {
        return element.className.match(new RegExp('(\\s|^)' + className +
                                                  '(\\s|$)'));
    } else {
        return false;
    }
}


/** Add 'className' to class definition of 'element'. **/

function addClass(element, className) {
    if(isString(element)) {
        element = elementByID(element);
    }    
    if (element.classList.add) {
        element.classList.add(className);
    } else if (element && className && (!hasClass(element, className)) ) {
        element.className += " " + className;
    }
}


/** Remove the 'class' from the class definition of 'element. **/

function removeClass(element, className) {
    if(isString(element)) {
        element = elementByID(element);
    }
    if (className == "") {
        // Empty class name so just ignore
        return;
    }
    if (element.classList.remove) {
        element.classList.remove(className);
    } else if(hasClass(element,className)) {
        var split = element.className.split(className);
        var newClass = "";
        for(var i=0;i<split.length;i++) {
            var trimmed = trim(split[i]);
            if(trimmed.length > 0) {
                newClass += newClass.length > 0 ? trimmed : trimmed + " ";
            }
        }
        element.className = newClass;
    }
}

function changeClassFromTo(element, from, to) {
    removeClass(element, from);
    addClass(element, to);
    return element;
}

/**
 * Adds a class if the add parameter is true if not removes it
 */
function yesNoClass(element, className, add){
    ((add === true)?addClass:removeClass).call(this, element, className)
}

/**
 * Clone a HTML node and update its ID and its children's IDs
 * by assigning 'index' as a number for
 * each.
 *
 * 'Prefix' will be a string attached to the front of each changed ID and
 *          can be used to add extra clarification to the created element ID
 *          to help with debugging (documenting what the element is used for).
 *          It can be null.
 *
 * The IDs to be replaced should be in the format: `<id>_#n`
 * (e.g. "surname_#n"). The _#n will be replaced with `_#<index>`. So the end
 * result will be: `<prefix><id>_#<index>`
 */

function cloneNodeSetIDs(node, prefix, index) {
    var newNode = node.cloneNode(true);

    setNumberIDs(newNode, index, prefix);
    return newNode;
}


/**
 * Set the indices for each element under 'node' with an ID in the format:
 * `<id>_#<index>`. If the ID of an element ends with "_#n", or if an index
 * of the form '_#<number>' alredy exists at the end, then that
 * part is changed to "_#<index>", otherwise that string is simply added
 * to the end.
 *
 * If 'prefix' is set, that is added to the front of each ID. Then the format
 * is:
 * `<prefix><id>_#<index>`
 *
 * If the prefix already exists, it is not added again.
 *
 * 'index' should be a number.
 */

function setNumberIDs(node, index, prefix) {
    var children, i, indexStart, endMatch, re, matchArray, begin;

    //console.log("setNumberIDS - node: " + node + ", index: " + index +
    //            ", prefix: " + prefix);
    if (prefix == undefined) {
        prefix = "";
    }

    // apply the change only if node.id  has a value
    // (node.id !== undefined && node.id !== "" && node.id !== null)
    if (node.id!==undefined && node.id!==null && node.id !== "") {
        if (node.id.substr(0, prefix.length) == prefix) {
            // The prefix is already there, do not readd it.
            prefix = "";
        }

        indexStart = node.id.length-3;
        endMatch = node.id.substr(indexStart, 3);

        if (endMatch == "_#n") {
            begin = node.id.slice(0, indexStart);
            node.id = prefix + begin + "_#" + index;
        } else {
            re = /_#\d+$/;
            matchArray = re.exec(node.id);
            if (matchArray != null && matchArray.length > 0) {
                // Found an index at the end, replace it.
                indexStart = node.id.length - matchArray[0].length;
                begin = node.id.slice(0, indexStart);
                node.id = prefix + begin + "_#" + index;
            } else {
                node.id = prefix + node.id + "_#" + index;
            }
        }
    }

    children = node.childNodes;
    for (i=0; i < children.length; i++) {
        setNumberIDs(children[i], index, prefix);
    }
}


/**
 * Find element based on the Stork ID components that are used to build a
 * unique ID when cloning etc. Use this to avoid assumptions on what
 * the final ID format is.
 *
 * 'startFromElement' start from this element and down into its children.
 *                    If this element matches the ID itself, it will be
 *                    returned.
 * 'prefix' if not null, the prefix of the ID to be used. As per
 *          setNumberIDs()
 * 'ID' the ID of the template that was used to build this.
 * 'index' the number index, as per setNumberIDs()
 */

function getElementFromStorkID(startFromElement, prefix, ID, index) {
    if (prefix == null) {
        prefix = "";
    }
    
    let fullID = prefix + ID + "_\\#" + index;

    if (startFromElement.id == fullID) {
        return startFromElement;
    }

    return startFromElement.querySelector("#" + fullID);
};


function updateNumberIDs(node, newIndex) {
    var re, indexStart, idPrefix;

    if (node.id != undefined) {
        re = /_#\d*$/;
        indexStart = node.id.search(re);
        if (indexStart != -1) {
            indexStart = node.id.search(re);
            idPrefix = node.id.substr(0, indexStart);
            node.id = idPrefix + "#_" + newIndex;
        }
    }
}


/**
 * Returns a copy of the object, by creating a new object and copying
 * each property and returning the new object. This does not do a deep
 * copy. Any objects properties point to are not copied, just the references
 */
function objectCopy(source) {
    var ob;

    if ( (source === undefined) || (source === null) ) {
        return source;
    }

    if (source instanceof Array) {
        ob = [];
    } else {
        ob = {};
    }

    for (var key in source) {
        ob[key] = source[key];
    }

    return ob;
};



function hideElement(element) {
    element.setAttribute("hidden", "");
    // Can be used in CSS for those browsers that don't support the 'hidden'
    // attribute
    addClass(element, "hidden");
    return;
};


function showElement(element) {
    element.removeAttribute("hidden", "");
    removeClass(element, "hidden");
    return;
};


function emptyElement(element) {
    //    children = element.childNodes;
    if(isString(element)) {
        element = elementByID(element);
    }
    if(!element || !element.hasChildNodes()) {
        return element;
    }

    var children = element.childNodes;
    while(element.hasChildNodes()) {
        element.removeChild(children[0]);
    }

    return element;
}


/**
 * Return the element for which the event was targeted.
 */
function getTargetElement(event) {
    if (window.event && window.event.srcElement) {
        // You're using IE. This is for you. But please get rid of it.
        return window.event.srcElement;
    } else {
        return event.currentTarget;
    }
}

function arrayIndexOf(array,item){
    if (!array){
        return -1;
    }
    var i = 0, l = array.length, nativeIndexOf = Array.prototype.indexOf;

    if (nativeIndexOf && array.indexOf === nativeIndexOf){
        return array.indexOf(item);
    }

    for (; i < l; i++){
        if (array[i] === item){
            return i;
        }
    }
    return -1;
}
/**
 * removeFromArray
 * var foo = function() { return "ARGH" };
 * var bar = function() { return "ÖÖRRR" };
 * var array = [foo, bar];
 * array = removeFromArray(array, foo);
 * @param array from which the object is going be removed
 * @param object that is to be removed from the array
 * @return the modified array
 */
function removeFromArray(array, object) {
    var idx=arrayIndexOf(array,object);
    if (idx>=0){
        array.splice(idx, 1);
    }
    return array;
}

/**
 * applyObjectsMethodWithParams
 * Iterates over list of objects and applies the given method with given parameters.
 * var arr = { listeners = [{ event: function() { alert(a + b); } },
 *                          { event: function() { this.concat = a + b; } }],
 *             emitEvent function() {
 *                 applyObjectsMethodWithParams(this.listeners, "event", [this, "a", "b"]);
 *             }
 *           };
 * arr.emitEvent();
 * @param objects list of objects
 * @param method string of the method name
 * @param params method parameters
 * @return array of called objects
 */
function applyObjectsMethodWithParams(objects, method, params) {
    var called = [];
    for(var i=0; i < objects.length; i++) {
        if(objects[i][method]) {
            objects[i][method].apply(objects[i], params);
            called.push(objects[i]);
        }
    }
    return called;
}



/**
 * Base object for all other StorkCore objects.
 *
 * @class StorkCore.Stork
 *
 * @singleton
 */
var Stork = clone(Object);


/**
 * @property {Array} copyProperties
 * List of properties to be copied on a clone
 */
Stork.copyProperties = [];

/**
 * @property {Array} cloneProperties
 * List of properties to clone
 */
Stork.cloneProperties = [];

/**
 * This is called on the new object after it was created by cloning.
 *
 * Can be used to define behaviour of properties during the cloning process.
 */
Stork.cloned = function() {
    var i, property;

    //this.listeners = objectCopy(this.listeners);
    this.copyProperties = objectCopy(this.copyProperties);
    for (i = 0; i < this.copyProperties.length; i++) {
        property = this.copyProperties[i];
        this[property] = objectCopy(this[property]);
    }

    this.cloneProperties = objectCopy(this.cloneProperties);
    for (i=0; i < this.cloneProperties.length; i++) {
        property = this.cloneProperties[i];
        this[property] = clone(this[property]);
    }
};


/**
 * Callback to initialize the clone. Must be explicitly called
 */
Stork.init = function() {
    return this;
};


/**
 * Use this to declare a property of this object which will have its
 * content automatically copied when the object is cloned.
 *
 * This should be used with e.g.
 * arrays that are not intended to be shared by all the clones of an object
 * (although they get the original copy). clone() will copy the property,
 * but with arrays and objects that is a reference to the object, thus
 * meaning every clone will share that same array or object. This can sometimes
 * be intentional, but often is not -- particularly with arrays. It is not
 * necessary to use this for simple values such as numbers, or for properties
 * which are just set with new objects (instead of changing the content of the
 * object).
 *
 * A deep copy is not made of these properties, so any values within the array
 * or object will not be copied. Only the array or object itself.
 *
 *
 * @param {string} property is the name of the property for this object (it will
 *          be made into a normal object property.
 * @param {Mixed} defaultValue is the value to set the property to when initialising.
 */
Stork.copiedProperty = function(property, defaultValue) {
    this[property] = defaultValue;
    this.copyProperties.push(property);
};


/**
 * Create a certain property of this object which should be automatically
 * cloned when the object is cloned. Normally the object pointed to by
 * a property would be shared by the two clones (the properties point to the
 * same obejct). Using clonedProperty() the object the property points to is
 * itself cloned.
 *
 * @param {string} property String name of the property that should be created,
 *       and marked to be auto-cloned (the object it points to is cloned).
 *
 * @param {Mixed} sourceObject The object from which clones will be made for
 *       'property' upon every clone of this Stork object.
 */
Stork.clonedProperty = function(property, sourceObject) {
    this[property] = sourceObject;
    this.cloneProperties.push(property);
};


/**
 * Basic event management to cover some browser differences.
 *
 * @class StorkCore.EventListenerRegistry
 * @extends StorkCore.Stork
 * @singleton
 */
var EventListenerRegistry = clone(Stork);
/**
 * @property {Object} transitionEndHandlers
 * registry of transitionEnd handlers
 */
EventListenerRegistry.transitionEndHandlers = {};

/**
 * identifies the browser support for css transitions and hooks up an element
 * listening for the transition event with an object wich provides
 * 'transitionEnd' method as the callback for transition end
 *
 * @param {HTMLElement} element HTML element to attach the transitionEnd event
 *          listener to
 * @param {Object} ob object containing a 'transitionEnd' method which will be
 *          used as callback
 * @return {Function/undefined} callback function created if there is support for
 *          transitions or undefined
 */
EventListenerRegistry.catchTransitionEnd = function(element, ob) {
    var my = this;
    if (! cssSupports("transitionProperty")) {
        // Transitions not supported by browser, so directly call the
        // handler. Transition happened immediately.
        ob.transitionEnd(element);
        return undefined;
    } else {
        var f = function() {
            ob.transitionEnd(element);
        };
        bean.add(element, 'transitionend webkitTransitionEnd oTransitionEnd', f);
        return f;
    }
};


/**
 * detaches the event listener for css transition end in browsers which support
 * them. This is used with callbacks set in {@link #catchTransitionEnd}
 *
 * @param {HTMLElement} element HTML element to attach the transitionEnd event
 *          listener to
 * @param {Object} ob object containing a 'transitionEnd' method which will be
 *          used as callback
 * @return {Function/undefined} callback function created if there is support for
 *          transitions or undefined
 */
EventListenerRegistry.uncatchTransitionEnd = function(element, f) {
    //    console.log("uncatchTransitionEnd - f: ");
    //console.log(f);
    if (! cssSupports("transitionProperty")) {
        return;
    }
    if (f == undefined) {
        return;
    }

    //    console.log("uncatchTransitionEnd - remove listeners");
    bean.remove(element, 'transitionend webkitTransitionEnd oTransitionEnd', f, false);
};




/**
 * Base for all model objects within StorkCore
 * @class StorkCore.StorkModel
 * @extends StorkCore.Stork
 * @singleton
 */
var StorkModel = clone(Stork);


/**
 * @property {Array} listeners
 * array of listeners registered for the model
 */
StorkModel.copiedProperty("listeners", []);
/**
 * @property {Object} modelProperties
 * object containing the properties and their vaules for the model
 */
StorkModel.copiedProperty("modelProperties", {});


/**
 * creates a model by cloning itself and applying to the clone all the
 * attributes existing in the object passed as argument
 *
 * @param {Object} object source object of the properties to be applied to the
 *      clone
 * @return {StorkCore.StorkModel} the clone of this model with all the
 *      properties from the 'object' argument applied
 */
StorkModel.createFromObject = function(object) {
    var model = clone(this);
    for (var key in object) {
        model.setProperty(key, object[key]);
    }
    return model;
};


/**
 * Add 'listener' to the list of objects that will be informed when
 * something is changed in this model.
 *
 * @param {Object} listener
 *    !verify an object with method 'propertyChangeEvent' as attribute, which
 *    will be used as callback when a property in the model changes
 *
 */
StorkModel.addListener = function(listener) {
    if (!listener){
        throw new Error("Empty object passed to StorkModel.addListener");
    }
    this.listeners.push(listener);
};


/**
 * Remove an object listening for changes in the attributes of this 
 * model. It is not an error to try to remove an object which is not a
 * listener.
 *
 * @param {Object} listener
 *    the object registered as listener to be deleted
 */
StorkModel.removeListener = function(listener) {
    this.listeners = removeFromArray(this.listeners, listener);
};


/**
 * applies to this model properties all the properties in an object passed as
 * argument
 *
 * @param {Object} properties object from which to set the properties into this
 *      model properties
 */
StorkModel.setProperties = function(properties) {
    for(var property in properties) {
        if(properties.hasOwnProperty(property)) {
            this.setProperty(property, properties[property]);
        }
    }
};


/**
 * Return an object containing each property of this model as a field in the
 * object.
 *
 * https://developer.mozilla.org/en/JSON#toJSON()_method
 *
 */
StorkModel.toJSON = function() {
    return this.modelProperties;
};

/**
 * sets the value for a property in the model. It also calls method to inform
 * listeners about the property value change
 *
 * @param {string} key name of the property to set a value to
 * @param {Mixed} value value to set to the property
 */
StorkModel.setProperty = function(key, value) {
    var oldValue = this.modelProperties[key];
    this.modelProperties[key] = value;
    var values = {
        oldValue: oldValue,
        value: value
    };
    console.debug(this.modelProperties);
    this._informListeners(key, oldValue, value);
};


/**
 * returns the value of a property
 *
 * @param {string} key name of the property to get the value from
 */
StorkModel.getProperty = function(key) {
    console.debug(this.modelProperties);
    return this.modelProperties[key];
};


/**
 * returns true if the model has the property
 *
 * @param {string} key name of the property to get the value from
 * @return {Boolean} true if this has the property
 */
StorkModel.hasProperty = function(key) {
    if (this.modelProperties[key] != undefined) {
        return true;
    } else {
        return false;
    }
};


StorkModel.cloned = function() {
    var i, property;

    this.superMethod(StorkModel.cloned, "cloned");
    //this.listeners = objectCopy(this.listeners);
    //this.copyProperties = objectCopy(this.copyProperties);
    for (i = 0; i < this.copyProperties.length; i++) {
        property = this.copyProperties[i];
        this.modelProperties[property] = 
            objectCopy(this.modelProperties[property]);
    }

    //this.cloneProperties = objectCopy(this.cloneProperties);
    for (i=0; i < this.cloneProperties.length; i++) {
        property = this.cloneProperties[i];
        this.modelProperties[property] = clone(this.modelProperties[property]);
    }
};


/**
 * @protected
 * calls all the listeners for the model properties value change. The listeners
 * should have a method named "propertyChangeEvent" which receives parameters
 *  (model, property, oldvalue, newValue)
 *
 * @param {string} property name of the property changed
 * @param {Mixed} oldValue old value of the property changed
 * @param {Mixed} newValue new value of the property changed
 */
StorkModel._informListeners = function(property, oldValue, newValue) {
    for(var i=0; i < this.listeners.length; i++) {
        if(this.listeners[i]["propertyChangeEvent"]) {
            this.listeners[i].propertyChangeEvent(this, property, oldValue,
                                                  newValue);
        }
    }
};


/* ****************************************************************************
 * Base for all controllers within Storkcore.
 *****************************************************************************/

/**
 * Base for all controller objects within StorkCore
 *
 * @class StorkCore.StorkController
 * @extends StorkCore.Stork
 * @singleton
 */
var StorkController = clone(Stork);

/**
 * @property {Array} listeners
 *      list of listeners attached to this controller
 */
StorkController.copiedProperty("listeners", []);
/**
 * @property {Array} keyBindings
 *      list of key bindings attached to the controller
 */
StorkController.copiedProperty("keyBindings", []);


/**
 * @property {StorkCore.StorkModel} model
 *      the StorkModel linked to this controller.
 */
StorkController.model = undefined;

/**
 * @property {Boolean} disabled
 *      true to make the controler disabled
 * @readonly
 */
StorkController.disabled = false;

/**
 * @property {Object} keyCodes
 *      map of the most used keycodes
 */
StorkController.keyCodes = {
    "13": "<<return>>",
    "27": "<<escape>>",
    "37": "<<left>>",
    "39": "<<right>>"
};


/**
 * Set up HTML 'element' to be the resulting view for this controller.
 * How exactly the controller manages the element depends on its implementation.
 *
 * This is a skeleton method. It should be overridden if more functionality
 * needs to take place on attachment.
 *
 * @param {HTMLElement/String} element
 *      HTML element or string with id of an htmlelement to attach the
 *      controller to
 */
StorkController.attach = function(element) {
    if(isString(element)) {
        element = elementByID(element);
    }
    if(!element) {
        throw "Can't attach to a undefined value!";
    }
    this.myElement = element;
    var my = this;
    this._keyFunc = function(e) {
        var keyCode;
        var key;

        if ( (e.which == undefined || e.which == 0) &&
             (e.charCode == undefined || e.charCode == 0) ) {
            // We don't deal with special keys for now
            return true;
        }

        if (e.keyCode == 0) {
            keyCode = e.charCode;
        } else {
            keyCode = e.keyCode;
        }

        // Check to see if the keycode matches the special keys from
        // the key table
        if (my.keyCodes[keyCode] != undefined) {
            key = my.keyCodes[keyCode];
        } else {
            key = String.fromCharCode(keyCode);
        }

        if ( (my.keyBindings[key] != undefined) &&
             (my.keyBindings[key].length != 0)) {
            // Keybinding available for this key
            for (var i=0; i < my.keyBindings[key].length; i++) {
                var bindingInfo = my.keyBindings[key][i];
                var ob = bindingInfo.target;
                var method = bindingInfo.method;
                ob[method](my, key);
            }

            if (e.preventDefault) {
                e.preventDefault();
            }
            if (e.stopPropagation) {
                e.stopPropagation();
            }

            return false;
        }
        return true;
    };
    bean.add(element, "keypress", this._keyFunc, true);

    this.isDisabled();
};

/**
 * Checks if the controller is disabled
 *
 * @returns {Boolean} true if the controller is disabled
 */
StorkController.isDisabled = function() {
    var disabled = this.disabled;
    if(this.myElement) {
        disabled = hasClass(this.myElement, "disabled") || this.myElement.disabled;
    }
    this.disabled = disabled;
    return disabled;
};

/**
 * Disables the controller
 */
StorkController.disable = function() {
    addClass(this.myElement, "disabled");
    if(this.myElement) {
        this.myElement.setAttribute("disabled", true);
    }
    this.disabled = true;
};

/**
 * Enables the controller
 */
StorkController.enable = function() {
    removeClass(this.myElement, "disabled");
    if(this.myElement.hasAttribute("disabled")) {
        this.myElement.removeAttribute("disabled");
    }
    this.disabled = false;
};

/**
 * Binds the pressing of a key to a method in an object.
 *
 * 'key' is the character we want to bind to (e.g. "k") or a key
 *       identifier. Currently only `<<return>>` is supported for a key
 *       identifier.
 * 'target' is the object that the callback method will be called on
 *          ('this' will point to it during the call).
 * 'methodName' is the name of the method to call, as a string.
 *
 * Once this bind has been made, normal actions for that key will not take
 * place. So if a form element is bound to `<<return>>`, the form will not
 * be submit when return is called, but the callback method will be called.
 * The character is case sensitive so "k" and "K" are different.
 *
 * @param {string} key
 *      (verify!) key code to bind
 * @param {HTMLElement/string} target
 *      (verify!) the target element receiving the keypress
 * @param {HTMLElement/string} methodName
 *      (verify!) methodName name of the method (in this controller) to use as
 *      callback when the key event happens
 *
 *  @return {Boolean} true if the key binding is correctly added
 */
StorkController.bindKey = function(key, target, methodName) {
    if (this.keyBindings[key] == undefined) {
        this.keyBindings[key] = [];
    }

    if(!key || !target || !methodName) {
        return false;
    }

    this.keyBindings[key].push({
        target: target,
        method: methodName
    });
    return true;
};


/**
 * Return the HTML element previously set with attach()
 * @return {HTMLElement} the HTML element previously set with attach()
 */
StorkController.getElement = function() {
    return this.myElement;
};

/**
 * Returns true if the controler has un underlying element
 * @return {Boolean} true if the controler has un underlying element
 */
StorkController.hasElement = function() {
    return this.myElement ? true : false;
};


/**
 * Return the model connected with this controller.
 *
 * @return {StorkCore.StorkModel/undefined}
 *      the model connected with this controller.
 */
StorkController.getModel = function() {
    return this.model;
};


/**
 * Add 'listener' to the list of objects that will be informed when
 * events happen.
 *
 * @param {Object} listener
 *      object with the listener methods to be added to ourlisteners
 */
StorkController.addListener = function(listener) {
    if (!listener) {
        throw new Error("Empty object passed to StorkModel.addListener");
    }
    this.listeners.push(listener);
};

/**
 * Remove listener from the list of objects that will be informed
 * of events.
 *
 * @param {Object} listener
 *      listener object to be removed to our listeners
 */
StorkController.removeListener = function(listener) {
    this.listeners = removeFromArray(this.listeners, listener);
};

/**
 * Inform listeners of a particular event
 *
 * @param {Event} event
 *      event to be informed about
 * @param {Object} eventParams
 *      (verify! if is object or array) parameters that will be given with
 *      the event
 */
StorkController.informListeners = function(event, eventParams) {
    applyObjectsMethodWithParams(this.listeners, event, eventParams);
};


/**
 * Focuses an element
 *
 * @param {HTMLElement} element
 *      HTML element to focus, if no element is passed the element for this
 *      controller is used
 */
StorkController.focus = function(element) {
    element = element || this.getElement();
    if(element) {
        element.focus();
    }
};


/**
 * Sets the model for this controller. Will add itself as a listener
 * to the model, to hear changes
 *
 * @param model
 */

StorkController.setModel = function(model) {
    if (this.model) {
        this.unsetModel();
    }
    this.model = model;
    this.model.addListener(this);
    if (this.myElement != undefined) {
        this.refresh();
    }
};


/**
 *
 *
 * @param model
 */
StorkController.unsetModel = function() {
    if (this.model) {
        this.model.removeListener(this);
    }
};


/**
 * Reacts to a change of any propert in the model attached to this item.
 */
StorkController.propertyChangeEvent = function(property, oldValue,
                                                  newValue) {
};


/* ****************************************************************************
 * StorkView is a base view for all views within Storkcore.
 * @class StorkCore.StorkView
 * @extends StorkCore.Stork
 *****************************************************************************/

var StorkView = clone(Stork);


/* The StorkModel linked to this controller. */
StorkView.model = undefined;


/**
 * Initialises the view. If it's cloned from a previous view that had a
 * model set, will apply to listen to that model.
 */

StorkView.cloned = function() {
    this.superMethod(StorkView.cloned, "cloned");
    if (this.model) {
        this.model.addListener(this);
    }
}


/**
 * Reacts to a change of any propertt in the model attached to this item.
 */
StorkView.propertyChangeEvent = function(property, oldValue,
                                         newValue) {
    // We're going to be lazy here and just refresh the whole thing.
    // Can be easily optimised when and if performance is an issue.
    this.refresh();
};


/**
 * Refresh content of the view. Is a no-op dummy at this level.
 */

StorkView.refresh = function() {
// Here we should probably go through all the objects that clone from this
// and refresh them too
}


/**
 * Sets the model for this controller. Will add itself as a listener
 * to the model, to hear changes
 *
 * @param model
 */

StorkView.setModel = function(model) {
    if (this.model) {
        this.unsetModel();
    }
    this.model = model;
    this.model.addListener(this);
    this.refresh();
};


/**
 * Removes model from this view. Removes view as listener of model.
 *
 * @param model
 */
StorkView.unsetModel = function() {
    if (this.model) {
        this.model.removeListener(this);
    }
};


/*****************************************************************************
 * StorkHtmlView is a special, but widely used view in storkCore, which
 * operates a HTML element. Provides ways to link StorkModels
 * directly to aspects of the HTML element. Thus actual rendering work is
 * kept, in most cases, to a minimum.
 *****************************************************************************/

var StorkHtmlView = clone(StorkView);


// The HTML element this view will be attached to
StorkHtmlView.element = undefined;

/* Index of element IDs in the HTML element used by this view. If -1,
   nothing has been set (this object might be using the original HTML,
   not the copied one).*/
StorkHtmlView.elementIndex = -1;

/**
 * @property {Array} listeners
 *      list of listeners attached to this controller
 */
StorkHtmlView.copiedProperty("listeners", []);
/**
 * @property {Array} keyBindings
 *      list of key bindings attached to the controller
 */
StorkHtmlView.copiedProperty("keyBindings", []);

/**
 * @property textProperties
 *      list of textual properties tied to a StorkModel
 */
StorkHtmlView.copiedProperty("textProperties", {});
/**
 * @property attrProperties
 */
StorkHtmlView.copiedProperty("attrProperties", {});
/**
 * @property attrMaps
 */
StorkHtmlView.copiedProperty("attrMaps", {});
/**
 * @property propertyMethods
 */
StorkHtmlView.copiedProperty("propertyMethods", {});
/**
 * @property textPropertyMethods
 */
StorkHtmlView.copiedProperty("textPropertyMethods", {});
StorkHtmlView.copiedProperty("classProperties", {});

/**
 * Initialises the view. If this object has a HTML element attached
 * to it (e.g. if it was cloned from an object that had that), then
 * that element will itself be cloned into a new element, but with all
 * 'id' attributes within the structure of that element changed to a new,
 * unique ones.
 *
 * The mapping from the original names to the new ones will, however,
 * be retained, so that the elements can be retreived with
 * StorkHtmlView.getMyElementByID()
 */

StorkHtmlView.cloned = function() {
    this.superMethod(StorkHtmlView.cloned, "cloned");
    if (this.element) {
        this.elementIndex = getUniqueID();
        this.element = cloneNodeSetIDs(this.element, "StorkHtmlView-",
                                       this.elementIndex);
    }
    return this;
}


/**
 * Return the element based on the original 'id'. If this view has been
 * cloned from one with a HTML element linked to it, the IDs will be
 * changed as part of cloning the original HTML element. This method
 * allows access to the equivalent elements in this clone.
 */

StorkHtmlView.getMyElementByID = function(id) {
    if (this.element == undefined) {
        return null;
    }

    if (this.elementIndex != -1) {
        id = "StorkHtmlView-" + id + "_#" + this.elementIndex;
    }

    if (this.element.parentElement) {
        return document.getElementById(id);
    } else {
        // The HTML element of this view is not in the document (yet), ie.
        // it's an orphan. Find the requested ID by going through the
        // HTML element.
        return containedElementByID(this.element, id);
    }
}


/**
 * Append the HTML of this view (that done with a previous attach()) as
 * a child of 'parentElement'.
 */

StorkHtmlView.appendToElement = function(parentElement) {
    if (this.element) {
        parentElement.appendChild(this.element);
    }
}


/**
 * Links a text property from the model to the content of an element. Note
 * that this completely empties the given element when the text property is
 * changed, and replaces the content with the model's property value.
 *
 * @param modelProperty
 *        String that gives the name of the property from the model to link to
 *
 * @param id
 *        ID of the element which should have its text content altered.
 */

StorkHtmlView.linkPropertyToText = function(modelProperty, id) {
    this.textProperties[modelProperty] = id;
};


/**
 * Links the result of a method call on the model to the textual content
 * of an element. Note that in this case there cannot be any automatic
 * updating from the model, but that the model itself has to inform
 * the controller (through refresh() or similar) of the change - in addition
 * to the update that happens as part of setModel().
 *
 * @param modelMethod
 *     Method to call on the StorkModel to get the text content.
 *
 * @param id
 *     ID of the element which should have its text content altered.
 */

StorkHtmlView.linkPropertyMethodToText = function(modelMethod, id) {
    this.textPropertyMethods[modelMethod] = id;
};


/**
 * Links a property from the model of this controller to a HTML attribute
 * of a HTML element.
 *
 * @param modelProperty
 *        String that gives the name of the property from the model to link to
 *
 * @param id
 *        ID of the element which should have its text content altered.
 *
 *
 * @param attributeName
 *        The attribute that will be changed, based on the content
 *        of the model property.
 */
StorkHtmlView.linkPropertyToAttr = function(modelProperty, id,
                                            attributeName) {
    this.attrProperties[modelProperty] = {
        id: id,
        attribute: attributeName
    };
};


/**
 * Map a set of possible model property values to equivalent values for
 * an attribute of an element.
 *
 * @param modelProperty
 *     The model property for which values should be checked.
 * @param id
 *     ID of the element we should set the attribute for.
 * @param attrName
 *     The attribute to be set in the HTML element.
 * @param propertyMap
 *     An object with each property key matching a possible value
 *     for 'modelProperty' and with the value being the value to set
 *     'attrName'.
 */

StorkHtmlView.mapPropertyToAttr = function(modelProperty, id,
                                           attrName, propertyMap) {
    this.attrMaps[modelProperty] = {
        id: id,
        attrName: attrName,
        propertyMap: propertyMap
    };
};


/**
 * Link changes in a model's property to a method of this object.
 *
 * @param modelProperty
 *               Model property to track.
 *
 * @param methodName
 *               The name of the method from this object to call when the
 *               model's property gets altered.
 *
 * @param elementIDMap
 *               If set, an object with each property mapped to an element
 *               ID. The property basically acts as a name for that
 *               desired element. So that if the method in 'methodName'
 *               wants to change the text of a field giving a car's colour,
 *               as well as the model of the car,
 *               it can do that by looking up elementIds["colourElement"].
 *               This elementIDMap will be passed to 'methodName'.
 *               Deprecated:
 *               If set, is a list of IDs for elements that should be passed
 *               to the method called.
 *               The actual elements will be looked up and passed to the method
 *               in its 'elements' argument.
 *
 *               If this is not defiend, 'elements' will not be passed to the
 *               method.
 *
 * The method will be called with either one or two arguments:
 * 'modelProperty' and optionally 'elementIDMap', as specified above.
 *
 */
StorkHtmlView.linkPropertyToMethod = function(modelProperty, methodName,
                                                elementIDMap) {
    this.propertyMethods[modelProperty] = {
        elementIDMap: elementIDMap,
        method: methodName
    };
};


/**
 * Links a property from the model to a CSS class of the attached element.
 * The value of the property will be added as a CSS class to the element,
 * changing as the property changes. 
*
 * @param classPrefix
 *        If !null, can be used to give the class a prefix when
 *        being set.
 */

StorkHtmlView.linkPropertyToClass = function(modelProperty, classPrefix) {
    console.debug("linkPropertyToClass called");
    console.debug(modelProperty);
    console.debug(this);
    if (classPrefix == undefined) {
        classPrefix = null;
    }
    
    this.classProperties[modelProperty] = {
        classPrefix: classPrefix,
        currentClass: ""
    };
    console.debug(this.classProperties);
    return;
};


/**
 * Set up HTML 'element' linked to this HTML view. That element will be a
 * HTML rendering of this view.
 *
 * @param {HTMLElement/String} element
 *      HTML element or string with id of an htmlelement to attach the
 *      controller to
 */

StorkHtmlView.attach = function(element) {
    if(isString(element)) {
        element = elementByID(element);
    }
    if(!element) {
        throw "Can't attach to a undefined value!";
    }
    this.element = element;
    this.elementIndex = -1;
    return;
};


/**
 * Return the HTML element that was set with attach(), or null if 
 * one was not set.
 */

StorkHtmlView.getAttachedElement = function() {
    return this.element;
};


/**
 * Do a full refresh of the values in the element.
 */
StorkHtmlView.refresh = function() {
    if (this.element == undefined) {
        // If the element hasn't been set yet, do nothing.
        return;
    }

    this._refreshTextProperties();
    this._refreshAttributesProperties();
    this._refreshMapAttributes();
    this._refreshPropertyMethods();
    this._refreshTextPropertyMethods();
    this._refreshPropertyClasses();
};


StorkHtmlView._refreshPropertyClasses = function() {
    console.debug("refresh property classes");
    console.debug(this.element);
    if (! this.element) {
        // No element set, so ignore
        return;
    }
    console.debug(this.classProperties);
    for (let key in this.classProperties) {
        let data = this.classProperties[key];
        removeClass(this.element, data.currentClass);
        console.debug(key);
        console.debug(this.model.getProperty(key));
        if (this.model.hasProperty(key)) {
            let value = this.model.getProperty(key);
            let prefix = "";
            if (data.classPrefix != null) {
                prefix = data.classPrefix;
            }
            this.classProperties[key].currentClass = prefix + value;
            addClass(this.element, prefix + value);
        }
    }
    return;
};


/**
 * @private
 */
StorkHtmlView._refreshTextProperties = function() {
    for (var key in this.textProperties) {
        if (this.textProperties[key] != undefined) {
            var elementID = this.textProperties[key];
            var textContainer = this.getMyElementByID(elementID);
            if (textContainer != null) {
                emptyElement(textContainer);
                var text = this.model.getProperty(key);
                textContainer.appendChild(document.createTextNode(text));
            }
        }
    }
};


/**
 * Go through the model's property methods, call them, and put the result
 * in the defined element.
 * @private
 */
StorkHtmlView._refreshTextPropertyMethods = function() {
    for(var key in this.textPropertyMethods) {
        if (this.textPropertyMethods[key] != undefined) {
            //var elementID = this.generateElementID(this.textPropertyMethods[key]);
            var elementID = this.textPropertyMethods[key];
            var textContainer = this.getMyElementByID(elementID);
            if (textContainer != null) {
                emptyElement(textContainer);
                var text = this.model[key].call(this.model, []);
                textContainer.appendChild(document.createTextNode(text));
            }
        }
    }
};


/**
 * @private
 */
StorkHtmlView._refreshAttributesProperties = function() {
    for (var key in this.attrProperties) {
        if (this.attrProperties[key] != undefined) {
            var data = this.attrProperties[key];
            var elementID = data["id"];
            var element = this.getMyElementByID(elementID);
            if (element != null) {
                var value = this.model.getProperty(key);
                element.setAttribute(data["attribute"], value);
            }
        }
    }
};


/**
 * @private
 */
StorkHtmlView._refreshMapAttributes = function() {
    for (var key in this.attrMaps) {
        var data = this.attrMaps[key];
        if (this.model.hasProperty(key)) {
            //var elementID = this.generateElementID(data["idPrefix"]);
            var elementID = data["id"];
            //console.log("createElement: elementID: " + elementID);
            var element = this.getMyElementByID(elementID);

            if (element != undefined) {
                var map = data["propertyMap"];
                var value = this.model.getProperty(key);
                var mappedValue = map[value];
                if (mappedValue != undefined) {
                    element.setAttribute(data["attrName"], mappedValue);
                }
            }
        }
    }
};


/**
 * Goes through the methods linked to properties to see if they need to be
 * called.
 * @private
 */
StorkHtmlView._refreshPropertyMethods = function() {
    for (var key in this.propertyMethods) {
        var elementID, element, i, methodElements, elementName;
        if (this.propertyMethods[key] != undefined) {
            var data = this.propertyMethods[key];

            if (data["elementIDMap"] != undefined) {
                this[data.method](key, data["elementIDMap"]);
            } else {
                this[data.method](key);
            }
        }
    }
};



/*****************************************************************************/
/* Specialised objects */

/**
 * Represents a single page. Ie. a combination of a number of other
 * elements and controls, with a main element covering all of them.
 *
 * @class StorkCore.StorkPage
 * @extends StorkCore.StorkController
 * @singleton
 */

var StorkPage = clone(StorkController);

/**
 * sets the model for this page
 *
 * @param {StorkCore.StorkModel} model for the page represented
 */
StorkPage.setModel = function(model) {
    this.model = model;
    this.update();
};

/**
 * updates the base controller once the model has been set into the model
 * property
 *
 * @abstract
 */
StorkPage.update = function() {
};

/**
 * Base view for any view which can have its events delegated to another
 * object.
 *
 * @class StorkCore.StorkViewDelegatable
 * @extends StorkCore.StorkView
 * @singleton
 *
 */
var StorkViewDelegatable = clone(StorkView);

/**
 * @property {StorkCore.StorkDelegate} delegate
 *      delegate Stork for the StorkViewDelegatable
 */
StorkViewDelegatable.delegate = undefined;

/**
 * Base delegate. Delegates should clone this.
 *
 * @class StorkCore.StorkDelegate
 * @extends StorkCore.Stork
 * @singleton
 */
var StorkDelegate = clone(Stork);


/**
 * Forms a collection of StorkViews displayed horizontally
 *
 * @class StorkCore.HorizontalCollectionView
 * @extends StorkCore.StorkView
 * @singleton
 */
var HorizontalCollectionView = clone(StorkView);

/**
 * @property {Array} children
 *      array of StorkCore.StorkView views which are children of the collection
 */
HorizontalCollectionView.children = [];

/*
 * overrides the method in the superclass
 */
HorizontalCollectionView.getDomElement = function() {
    var div = document.createElement("div");
    for (var i = 0; i < this.children.length; i++) {
        div.appendChild(this.children[i].getDomElement());
    }
    return div;
};


/**
 * delegate for select-type views
 *
 * @class StorkCore.OptionDelegatePrototype
 * @extends StorkCore.StorkDelegate
 * @singleton
 */
var OptionDelegatePrototype = clone(StorkDelegate);

/**
 * @property {Array} options
 *
 * List of options for the delegate.
 *
 * Each option should be an option with properties:
 *
 *  * 'value'  this is the "value" attribute in an option
 *  * 'text'  this is what is shown to the user. To
 *  * 'selected'  [optional] true if the item is selected
 *
 *          @example
 *          [{ value: "Foo", text: "Foo"},
 *           { value: "Bar", text: "Bar", selected:true}]
 *
 *
 */
OptionDelegatePrototype.options = [];


/**
 * Controller for all views which can select from a set of views to be shown.
 *
 * @class StorkCore.ViewSelectController
 * @extends StorkCore.StorkDelegate
 * @singleton
 */
var ViewSelectController = clone(StorkDelegate);

/**
 * @cfg{Array} viewOptions
 *
 * List of objects which define the titles of the various views to be shown,
 * plus an ID for each. Each ID is a unique identifier for the view to be
 * shown, passed to {@link #getElementForSelection}
 *
 *     @example
 *     [{text: "View 1", value: "view1"},
        {text: "View 2", value: "view2"}]
 */
ViewSelectController.viewOptions = [{text: "View 1", value: "view1"},
                                    {text: "View 2", value: "view2"}];

/**
 * @cfg {Object} elements
 * In this implementation each 'view' is just a HTML element.
 *
 * Each option listed with viewOptions should have a similar HTML
 * node linked to it.
 *
 * These elements should be orphaned (removed from the normal HTML document
 * flow, away from parent).
 *
 *     @example
 *     {view1: document.createTextNode("View 1"),
 *     view2: document.createTextNode("View 2")}
 *
 */

ViewSelectController.elements = {};

/**
 * @cfg {String} selectedView
 *
 * Set this to the ID of the view we want to show.
 */
ViewSelectController.selectedView = "view1";


/**
 * @cfg {HTMLElement} defaultView
 *
 * HTMLElement representing the default view
 */
ViewSelectController.defaultView = document.createTextNode("");


/**
 *
 * Sets a mapping between a view ID, as set in viewOptions, and a DOM element
 * ID which should be displayed when that view ID is selected.
 *
 * This function also removes the element from its parent (making it an orphan)
 * so it is no longer displayed in the document flow, until the option is
 * selected (and then it will appear as part of whichever view wants to
 * display it).
 *
 * @param {String} viewID
 *      DOM id of the view to set the element to
 * @param {String} elementID
 *      DOM id of the element to set in the view
 */

ViewSelectController.setViewElement = function(viewID, elementID) {
    var element = document.getElementById(elementID);
    this.elements[viewID] = element;
    element.parentNode.removeChild(element);
};


/**
 * Returns an element that should be shown for the given view ID.
 *
 * The behaviour in this prototype is just to return one of the elements
 * listed in this.elements. Naturally this can be expanded on for other
 * objects.
 *
 * @param {String} selection
 *      the view id to select
 *
 * @return {HTMLElement/Array}
 *      the default view HTML Element or the elements for the selected view
 *      passed as parameters
 */
ViewSelectController.getElementForSelection = function(selection) {
    if (this.elements[selection] == undefined) {
        return this.defaultView;
    }

    return this.elements[selection];
};

/**
 * This is a view where the user can select from a bunch of sub-views by
 * clicking on one of a list of view titles.
 *
 * The widget has two modes, tabled rendering, or div rendering. The first
 * is so that the selection options can be placed in a box to the left which
 * is of equal size as any content that might appear on the right.
 * The div version (default) just has a list of subsequent divs to show the
 * content.
 *
 * CSS elements:
 *  * __sc-clickViewBorder__: The outer div covering everything inside this
 *    widget.
 *  * __selection__: The area where the selection options are displayed.
 *  * __content__: The 'content', ie. the view to be shown for whichever section
 *    is selected will appear in this.
 *  * __selection_option-selected, selection_option-unselected__: `<div>`s and
 *    `<a>`s for an option which is selected or not selected.
 *
 *  @class StorkCore.ClickViewSelect
 *  @extends StorkCore.StorkViewDelegatable
 *  @singleton
 */
var ClickViewSelect = clone(StorkViewDelegatable);

ClickViewSelect.showedElement = undefined;

ClickViewSelect.delegate = ViewSelectController;

// The element that represents a selected tab
ClickViewSelect.currentlySelectedElement = undefined;

// If this is set, the selection options and the content view will be
// separated into two cells in a table. Necessary for sidebar implementation
// as pure CSS is quite limited in what you can do for that. If this is false
// a set of DIVs will be used instead.
ClickViewSelect.tabled = false;

ClickViewSelect.toString = function() {
    return "ClickViewSelect";
};

ClickViewSelect.handleEvent = function(event) {
    if (! this.showedElement) {
        return false;
    }
    if (! this.delegate) {
        return false;
    }

    var selectedElement = getTargetElement(event);

    var nextShow = this.delegate.getElementForSelection(selectedElement.id);
    if (nextShow != this.showedElement) {
        this.showedElement.parentNode.appendChild(nextShow);
        this.showedElement.parentNode.removeChild(this.showedElement);
    }
    this.showedElement = nextShow;
    this.currentlySelectedElement.className = "selection_link-unselected";
    this.currentlySelectedElement.parentNode.className = "selection_option-unselected";
    selectedElement.className = "selection_link-selected";
    selectedElement.parentNode.className = "selection_option-selected";
    this.currentlySelectedElement = selectedElement;

    return false;
};

ClickViewSelect.getDomElement = function() {
    var me = this;
    var outerDiv = document.createElement("div");
    outerDiv.className = "sc-clickViewBorder";
    var selectionElement;
    var contentElement;
    if (this.tabled) {
        var table = document.createElement("table");
        table.className = "sc-clickViewSelection";
        var tbody = document.createElement("tbody");
        table.appendChild(tbody);

        var row = document.createElement("tr");
        row.className = "sc-clickViewSelection";

        selectionElement = document.createElement("td");
        row.appendChild(selectionElement);

        contentElement = document.createElement("td");
        row.appendChild(contentElement);

        tbody.appendChild(row);
        outerDiv.appendChild(table);
    } else {
        selectionElement = document.createElement("div");
        contentElement = document.createElement("div");
        outerDiv.appendChild(selectionElement);
        outerDiv.appendChild(contentElement);
    }

    selectionElement.className = "selection";
    contentElement.className = "content";

    if (this.delegate != undefined) {
        var viewOptions = this.delegate.viewOptions;
        for (var i = 0; i < viewOptions.length; i++) {
            var optionInfo = viewOptions[i];
            var selectionOptionDiv = document.createElement("div");
            selectionOptionDiv.className = "selection_option-unselected";

            // Build link that user can click to switch view
            var link = document.createElement("a");
            selectionOptionDiv.appendChild(link);
            link.setAttribute("href", "#");
            link.className = "selection_link-unselected";
            link.id = optionInfo.value;

            link.onclick = function(event) {
                return me.handleEvent(event);
            };

            if (optionInfo.value == this.delegate.selectedView) {
                // This view was the currently selected one according to
                // delegate
                selectionOptionDiv.className =
                    "selection_option-selected";
                link.className =
                    "selection_link-selected";
                this.showedElement =
                    this.delegate.getElementForSelection(optionInfo.value);
                this.currentlySelectedElement = link;
            }
            link.appendChild(document.createTextNode(optionInfo.text));
            selectionElement.appendChild(selectionOptionDiv);
        }
    }

    if (this.currentlySelectedElement == undefined) {
        // Nothing was specified as currently selected, go with first one
        var firstElement = selectionElement.childNodes[0].childNodes[0];
        if (firstElement == undefined) {
            return;
        }
        firstElement.className = "selection_link-selected";
        this.showedElement =
            this.delegate.getElementForSelection(firstElement.id);
        this.currentlySelectedElement = firstElement;

    }

    // Manage content
    if (this.showedElement) {
        contentElement.appendChild(this.showedElement);
    }

    // Required clearing div if the CSS defines selection and content divs
    // to be floating.

    var clearingDiv = document.createElement("div");
    clearingDiv.className = "finish";
    outerDiv.appendChild(clearingDiv);

    return outerDiv;
};


/***************************************************************************
 * A dialog with sections and buttons at the bottom.
 ***************************************************************************/

var SectionedDialog = clone(ClickViewSelect);

SectionedDialog.buttons = [
    {
        name: "ok",
        value: "Ok"
    },
    {
        name: "cancel",
        value: "Cancel"
    }
];

SectionedDialog.getDomElement = function() {
    var dom = ClickViewSelect.getDomElement.call(this);
    dom.className = "sc-sectioned_dialog";
    var buttonDiv = document.createElement("div");
    buttonDiv.className = "buttons";

    for (var i=0; i < this.buttons.length; i++) {
        var button = document.createElement("input");
        button.setAttribute("type", "submit");
        button.name = this.buttons[i].name;
        button.value = this.buttons[i].value;
        buttonDiv.appendChild(button);
    }
    dom.appendChild(buttonDiv);
    return dom;
};



/*****************************************************************************
 * A collection of various views which can be switched by the user by
 * selecting from a dropdown. Only
 * one is visible at a time.
 *****************************************************************************/

var DropDownViewSelect = clone(StorkViewDelegatable);

DropDownViewSelect.select = undefined;

DropDownViewSelect.showedElement = undefined;

DropDownViewSelect.delegate = ViewSelectController;


DropDownViewSelect.handleEvent = function(event) {
    if (! this.select || ! this.showedElement) {
        return;
    }
    if (! this.delegate) {
        return;
    }

    var selected = this.select.options[this.select.selectedIndex].value;
    var nextShow = this.delegate.getElementForSelection(selected);
    this.showedElement.parentNode.appendChild(nextShow);
    this.showedElement.parentNode.removeChild(this.showedElement);
    this.showedElement = nextShow;
    return;
};


DropDownViewSelect.getDomElement = function() {
    var outerDiv = document.createElement("div");
    outerDiv.className = "sc-dropDownViewBorder";
    var titleDiv = document.createElement("div");
    titleDiv.className = "sc-dropDownViewTitleBox";
    outerDiv.appendChild(titleDiv);

    this.select = document.createElement("select");
    this.select.className = "sc-dropDownViewSelect";
    this.select.onchange = this;

    if (this.delegate != undefined) {
        var viewOptions = this.delegate.viewOptions;
        if (viewOptions.length > 0) {
            // Use first view as default if nothing else specified as selected
            this.showedElement =
                this.delegate.getElementForSelection(viewOptions[0].value);
        }

        var selected = this.delegate.selectedView;
        for (var i = 0; i < viewOptions.length; i++) {
            // Generate one select option for each delegate viewOptions.
            // Set view of DropDownViewSelect if any of them is selected.
            var optionInfo = viewOptions[i];
            var option = document.createElement("option");
            option.setAttribute("value", optionInfo.value);
            option.setAttribute("style", "color: red;");
            if (optionInfo.value == selected) {
                option.setAttribute("selected");
                this.showedElement =
                    this.delegate.getElementForSelection(optionInfo.value);
            }
            option.appendChild(document.createTextNode(optionInfo.text));
            this.select.appendChild(option);
        }
    }

    titleDiv.appendChild(this.select);

    var contentDiv = document.createElement("div");
    contentDiv.className = "sc-dropDownViewContent";
    if (this.showedElement) {
        contentDiv.appendChild(this.showedElement);
    }
    outerDiv.appendChild(contentDiv);

    return outerDiv;
};



/*****************************************************************************
 * Drop-down options
 ************************************************************************/

var OptionPrototype = clone(StorkView);

OptionPrototype.name = "name";

OptionPrototype.delegate = OptionDelegatePrototype;


OptionPrototype.getDomElement = function() {
    var select = document.createElement("select");
    select.setAttribute("name", this.name);

    for (var i = 0; i < this.delegate.options.length; i++) {
        var option = document.createElement("option");
        var optionInfo = this.delegate.options[i];
        option.setAttribute("value", optionInfo.value);
        if (optionInfo.selected != undefined) {
            option.setAttribute("selected");
        }
        option.appendChild(document.createTextNode(optionInfo.text));
        select.appendChild(option);
    }

    return select;
};



var FormViewPrototype = clone(StorkView);

FormViewPrototype.element = undefined;

FormViewPrototype.render = function() {
    if (this.element == undefined) {
        return;
    }
    emptyElement(this.element);

    var table = document.createElement("table");
    table.className = "formTable";
    var tbody = document.createElement("tbody");
    table.appendChild(tbody);

    for (var i=0; i < this.delegate.fields.length; i++) {
        var field = this.delegate.fields[i];
        var row = document.createElement("tr");
        var cell = document.createElement("td");
        var label = document.createElement("label");
        label.className = "sc_label";

        var text = document.createTextNode(field + ":");
        label.appendChild(text);
        cell.appendChild(label);
        row.appendChild(cell);
        if (this.delegate.fieldObs[field] != undefined) {
            cell = document.createElement("td");
            cell.appendChild(this.delegate.fieldObs[field].getDomElement());
            row.appendChild(cell);
        }
        table.appendChild(row);
    }

    this.element.appendChild(table);
};


var FormDelegatePrototype = clone(Stork);

FormViewPrototype.delegate = FormDelegatePrototype;

FormDelegatePrototype.fieldObs = new Object();

FormDelegatePrototype.setField = function(field, storkOb) {
    this.fieldObs[field] = storkOb;
    return;
};

/*************/

var StringView = clone(Stork);

StringView.myText = "";

StringView.text = function(newText) {
    this.myText = newText;
    return this;
};

StringView.getDomElement = function() {
    return document.createTextNode(this.myText);
};


/*** Input field ***/

var InputFieldPrototype = clone(Stork);

InputFieldPrototype.name = "name";
InputFieldPrototype.size = "15";

InputFieldPrototype.getDomElement = function() {
    var field = document.createElement("input");
    field.setAttribute("name", this.name);
    field.setAttribute("type", "text");
    field.setAttribute("size", this.size);

    return field;
    /*          <input type="text" name="date" id="field_date"
     value="{% now "Y-m-d" %}" size="10" maxlength="10"> */
};


function _storkCore_includesLoaded() {
    if (typeof(storkCore_loadedCallback) != "undefined") {
        storkCore_loadedCallback();
    }
}

var MAX_DUMP_DEPTH = 5;

function dumpObj(obj, name, indent, depth) {
    if (depth > MAX_DUMP_DEPTH) {
        return indent + name + ": <Maximum Depth Reached>\n";
    }
    if (typeof obj == "object") {
        var child = null;
        var output = indent + name + "\n";
        indent += "\t";
        for (var item in obj)
        {
            if(!obj.hasOwnProperty(item)) {
                continue;
            }
            try {
                child = obj[item];
            } catch (e) {
                child = "<Unable to Evaluate>";
            }
            if (typeof child == "object") {
                output += dumpObj(child, item, indent, depth + 1);
            } else {
                output += indent + item + ": " + child + "\n";
            }
        }
        return output;
    } else {
        return obj;
    }
}