storkCore

Artifact [120b5c7c9d]
Login

Artifact [120b5c7c9d]

Artifact 120b5c7c9d7f784e2ad20f845425bb969a65937d:


/**
 * Controller for list widgets with items
 *
 * @class StorkUtil.ListController
 * @extends StorkCore.StorkController
 * @singleton
 */
var ListController = clone(StorkController);

/**
 * @property {Array} itemControllers
 */
ListController.copiedProperty("itemControllers", []);

/**
 * @property {HTMLElement} containerElement
 */
ListController.containerElement = undefined;
/**
 * @property itemControllerProto
 */
ListController.itemControllerProto = undefined;

/**
 * @property {Number} nextItemID
 * The ID for the next item created within this list. Each ID will be
 * unique for the existence of this ListController
 */
ListController.nextItemID = 1;
ListController.nextListID = 1;

/**
 * @property zeroLengthElement
 */
ListController.zeroLengthElement = undefined;

/**
 * Set the element which will contain the items of the list.
 */
ListController.setListContainer = function(element) {
    if(isString(element)) {
        element = elementByID(element);
    }
    this.containerElement = element;
};


/**
 * Set the HTML element to be shown if the list has no items. This can be
 * useful for informational purposes. If this is not set, or is 'undefined',
 * just show an empty list.
 *
 * Visibility of the two elements is controlled thus:
 * If this is set, and the list is empty, the class for the normal list
 * container element (set with setListContainer) will be set to 'hidden'.
 * If the list contains elements, the container element 'hidden' class will
 * be removed, and 'hidden' class added to the element passed into this method.
 */
ListController.setZeroLengthElement = function(element) {
    this.zeroLengthElement = element;
};


/**
 * 'itemController' is the StorkCore object which will be cloned as a
 * controller for each item in the list.
 */
ListController.setItemControllerPrototype = function(itemController) {
    this.itemControllerProto = itemController;
};

ListController.unsetModel = function(){
    for (var i=0; i<this.itemControllers.length;i++){
        this.itemControllers[i].unsetModel();
        this.itemControllers[i].deleted();
    }
};

/**
 * Sets 'listModel' (a ListModel object) to be the model for this list.
 * Each element in the model will be given its own controller, cloned from one
 * set with setItemControllerPrototype() - which must be called before
 * calling this.
 *
 * init() will not be called on this clone.
 *
 * Each item controller will get a StorkModel based on the content of the
 * element of 'listModel'.
 */
ListController.setModel = function(listModel) {
    if(!this.itemControllerProto) {
        throw "You need to set the item controller prototype (setItemControllerPrototype) before calling setModel!";
    }
    if(!this.containerElement) {
        throw "You need to set the list container element (setListContainer) before calling setModel!";
    }

    //remove the listeners to the current model if it exists, it can lead to
    //infinite recursion if resetEvent method is not overriden
    if (this.model){
        this.model.removeListener(this);
        // process the model unsetting
        this.unsetModel();
    }

    this.model = listModel;
    listModel.removeListener(this);
    emptyElement(this.containerElement);
    var itemAmount = listModel.getLength();

    if (this.listID === undefined){
        this.listID=ListController.nextListID;
        ListController.nextListID++;
    }

    this.showOrHideZeroLengthElement();

    this.itemControllers = [];
    for (var i = 0; i < itemAmount; i++) {
        var itemController = clone(this.itemControllerProto);
        //itemController.init();
        //console.log("Model for item " + i + ":", listModel.getItemModel(i));
        itemController.setModel(listModel.getItemModel(i));
        itemController.setListID(this.listID);
        itemController.setListItemID(this.nextItemID);
        itemController.setListModel(listModel);
        this.nextItemID++;
        itemController.createElement(document, this.containerElement);
        itemController.drawed();
        this.itemControllers[i] = itemController;
        this.informListeners('listItemControllerAdded',[this,
                                                        itemController,
                                                        i]);
    }

    listModel.addListener(this);
};


/**
 * Check to see if the zero length element should be shown or not.
 * Show it if it is set, and the length of the list is 0.
 */
ListController.showOrHideZeroLengthElement = function() {
    var itemAmount = this.model.getLength();

    if (this.zeroLengthElement != undefined) {
        if (itemAmount == 0) {
            addClass(this.containerElement, "hidden");
            removeClass(this.zeroLengthElement, "hidden");
        } else {
            removeClass(this.containerElement, "hidden");
            addClass(this.zeroLengthElement, "hidden");
        }
    }
};

/**
 *
 */
ListController.resetEvent = function(listModel) {
    this.setModel(listModel);
};
/**
 *
 */
ListController.sortEvent = function(listModel) {
    var itemControllers=this.itemControllers.slice();
    var sortedControllers=[];
    var i;
    var orderChanged=false;
    for (i=0;i<listModel.length;i++){
        var model=listModel[i];
        for (var j=0;j<itemControllers.length;j++){
            var ctrlr=itemControllers[j];
            if (model==ctrlr.getModel()){
                sortedControllers.push(ctrlr);
                itemControllers.splice(j,1);
                // if j is always 0 then the order hasn't changed
                if (!orderChanged && j!==0){
                    orderChanged=true;
                }
                break;
            }
        }
    }
    // reorder (move nodes to the end in order)
    if (orderChanged===true){
        for (i=0;i<sortedControllers.length;i++){
            this.containerElement.appendChild(sortedControllers[i].getElement());
        }
    }

};

ListController.deleteItemEvent = function(source, item, index) {
    var itemController = this.itemControllers[index];
    if(itemController) {
        this.itemControllers.splice(index, 1);
        itemController.deleted();
        this.informListeners('listItemControllerDeleted',
                             [this, itemController]);
        this.showOrHideZeroLengthElement();
    }
};


/**
 * React to 'insertBefore' event coming from model.
 * Clones a new ListItemController based on the set with
 * setItemControllerPrototype() for the newly added item, and then
 * displays the applicable HTML element at the appropriate location in the
 * DOM tree (before the element representing 'index').
 *
 * 'source' is the source ListModel for the change.
 * 'modelItem' is the newly added model representing the new item.
 * 'index' is the index at which it appeared.
 */
ListController.insertBeforeEvent = function(source, modelItem, index) {
     /*console.log("ListController.insertBeforeEvent:", source, modelItem, index);
     console.log(this.itemControllerProto);
     console.log("modelItem: ");
     console.log(modelItem);*/
    var itemController = clone(this.itemControllerProto);
    //itemController.init();
    itemController.setModel(modelItem);
    itemController.setListID(this.listID);
    itemController.setListItemID(this.nextItemID);
    this.nextItemID++;
    var nextController = this.itemControllers[index];
    if(nextController) {
        var nextElement = nextController.getElement();
        /*console.log("insertBeforeEvent - nextController.getElement:");
         console.log(nextElement);*/
        itemController.createElement(document, this.containerElement, nextElement);
        this.itemControllers.splice(index, 0, itemController);
    }
    else {
        /*console.log(this.containerElement);*/
        itemController.createElement(document, this.containerElement);
        this.itemControllers.push(itemController);
    }

    itemController.setListModel(this.model);
    itemController.drawed();

    this.informListeners('listItemControllerAdded',[this,
                                                    itemController,
                                                    index]);
    /*console.log("insertBeforeEvent - itemControllers.length: " +
     this.itemControllers.length);*/

    this.showOrHideZeroLengthElement();
};


/**
 * Controller for list items contained within lists
 *
 * @class StorkUtil.ListItemController
 * @extends StorkCore.StorkController
 * @singleton
 */
var ListItemController = clone(StorkController);

/**
 * @property itemTemplate
 */
ListItemController.itemTemplate = undefined;
/**
 * @property listID
 */
ListItemController.listID = undefined;
/**
 * @property model
 */
ListItemController.model = undefined;
/**
 * @property listModel
 */
ListItemController.listModel = undefined;

/**
 * @property myElement
 * The element representing this item
 */
ListItemController.myElement = undefined;

 /**
  * @property IDPrefix
  * Prefix to use with all IDs inside the DOM sub-tree that this controller
  * is linked to. Each node in the template will be replaced to start with this
  * prefix.
  */
ListItemController.IDPrefix = undefined;
/**
 * @property textProperties
 */
ListItemController.copiedProperty("textProperties", {});
/**
 * @property attrProperties
 */
ListItemController.copiedProperty("attrProperties", {});
/**
 * @property attrMaps
 */
ListItemController.copiedProperty("attrMaps", {});
/**
 * @property propertyMethods
 */
ListItemController.copiedProperty("propertyMethods", {});
/**
 * @property textPropertyMethods
 */
ListItemController.copiedProperty("textPropertyMethods", {});

/**
 * Get the full, actual element ID for the given 'id'. This is formed from
 * an internal prefix, the given id, "_#" and the list ID.
 */
ListItemController.getFullElementID = function(id) {
    return this.IDPrefix + id + "_#" + this.getListItemID();
};


/**
 * The ListModel of which this item's model is a part. Usually set by
 * the ListController.
 */
ListItemController.setListModel = function(listModel) {
    this.listModel = listModel;
};


/**
 * @param {Number} listID the index of the item within the list.
 */
ListItemController.setListID = function(listID) {
    this.listID = listID;
};


/**
 * Return the ID of this item from within the ListModel that it is
 * contained. This ID is unique for each item created under the ListModel
 * it belongs to. Note: it is not the same as the index within that list.
 */

ListItemController.getListID = function() {
    return this.listID;
};

/**
 * 'listID'  - The index of the item within the list.
 */

ListItemController.setListItemID = function(listItemID) {
    this.listItemID = listItemID;

    if (this.myElement) {
        updateNumberIDs(this.myElement, listItemID);
    }
};


/**
 * Return the ID of this item from within the ListModel that it is
 * contained. This ID is unique for each item created under the ListModel
 * it belongs to. Note: it is not the same as the index within that list.
 */
ListItemController.getListItemID = function() {
    return this.listItemID;
};

/**
 *
 */
ListItemController.generateElementID = function(idPart) {
    return this.IDPrefix + idPart + "_#" + this.listItemID;
};

/**
 * Called when ListController has finished drawing this item.
 * Can be used to do extra work for that particular list item.
 * Adds also selection event to controller so if the item is
 * clicked a "listItemSelected" event is emited to listeners.
 */
ListItemController.drawed = function() {
    var element = this.getElement();
    var model = this.model;
    if(element && model) {
        var btn = clone(ButtonController);
        btn.attach(element);
        var my = this;
        btn.buttonPressed = function(e) {
            applyObjectsMethodWithParams(my.listeners, "listItemSelected",
                                         [my, model]);
        };
    }
};

/**
 * Sets the item to use the HTML element 'itemTemplate' as a template for
 * when it will be rendered. 'itemTemplate' will be orphanised from wherever
 * it currently is (ie. it will be removed from its parent).
 *
 * The template should have node IDs, ending with _#n, which will then be
 * linked up to model properties, using linkTextProperty() or similar.
 */
ListItemController.setItemTemplate = function(itemTemplate) {
    if(isString(itemTemplate)) {
        itemTemplate = elementByID(itemTemplate);
    }
    this.itemTemplate = itemTemplate;
};


/**
 * Link a property, which is assumed to be text, from the model of this
 * list item to the content of an element specified with 'idPrefix'.
 * The ID should end with "_#n" after 'idPrefix'.
 */
ListItemController.linkTextProperty = function(modelProperty, idPrefix) {
    this.textProperties[modelProperty] = idPrefix;
};


/**
 * Should replace this with mapping model get() to also worth with methods.
 */
ListItemController.linkTextPropertyMethod = function(modelProperty, idPrefix) {
    this.textPropertyMethods[modelProperty] = idPrefix;
};


/**
 * Links a property from the model of this list item to a HTML attribute
 * from the element with the ID prefixed 'idPrefix', and ending in "_#n".
 *
 * 'attributeName' is the attribute that will be changed, based on the content
 *                 of the model property.
 */
ListItemController.linkAttrProperty = function(modelProperty, idPrefix,
                                               attributeName) {
    this.attrProperties[modelProperty] = {
        idPrefix: idPrefix,
        attribute: attributeName
    };
};


/**
 * Map a set of possible model property values to equivalent values for
 * an attribute with the ID prefixed 'idPrefix', and ending in "_#n".
 *
 * 'modelProperty' is the model property for which values should be checked.
 * 'idPrefix' is the ID prefix of the element we should set the attribute for.
 * 'attrName' is the attribute to be set in the HTML element.
 * 'propertyMap' is an object with each property key matching a possible value
 *               for 'modelProperty' and with the value being the value to set
 *               'attrName'.
 */
ListItemController.mapPropertyToAttr = function(modelProperty, idPrefix,
                                                attrName, propertyMap) {
    this.attrMaps[modelProperty] = {
        idPrefix: idPrefix,
        attrName: attrName,
        propertyMap: propertyMap
    };
};


/**
 * Link changes in 'modelProperty' to a method, that should be set in a clone
 * of this ListItemController.
 *
 * 'elementIDs', if set, is a list of IDs for elements that should be passed
 *               to the method called. In the template (set with
 *               setItemTemplate()) the IDs should be of the form <id>_#n.
 *               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 'elements', as specified above.
 *
 */
ListItemController.linkPropertyToMethod = function(modelProperty, methodName,
                                                   elementIDs) {

    this.propertyMethods[modelProperty] = {
        idPrefixes: elementIDs,
        method: methodName
    };
};


/**
 * Do a full refresh of the values in the element.
 */
ListItemController.refresh = function() {
    if (this.myElement == undefined) {
        // If the element hasn't been set yet, do nothing.
        return;
    }
    //console.debug("refresh", this.myElement);
    this._refreshTextProperties();
    this._refreshTextPropertyMethods();
    this._refreshAttributesProperties();
    this._refreshMapAttributes();
    this._refreshPropertyMethods();
};
/**
 * @private
 */
ListItemController._refreshTextProperties = function() {
    for (var key in this.textProperties) {
        //console.log("createElement, key: " + key);
        if (this.textProperties[key] != undefined) {
            //console.log("found property: " + key);
            //var elementID = this.textProperties[key] + "_#n";
            var elementID = this.generateElementID(this.textProperties[key]);
            //console.log("createElement, elementID:");
            //console.log(elementID);
            var textContainer = document.getElementById(elementID);
            if (textContainer != null) {
                emptyElement(textContainer);
                var text = this.model.getProperty(key);
                //console.log("createElement - text: " + text);
                textContainer.appendChild(document.createTextNode(text));
            }
        }
    }
};

/**
 * @private
 */
ListItemController._refreshTextPropertyMethods = function() {
    for(var key in this.textPropertyMethods) {
        if (this.textPropertyMethods[key] != undefined) {
            var elementID = this.generateElementID(this.textPropertyMethods[key]);
            var textContainer = document.getElementById(elementID);
            if (textContainer != null) {
                emptyElement(textContainer);
                var text = this.model[key].call(this.model, []);
                textContainer.appendChild(document.createTextNode(text));
            }
        }
    }
};

/**
 * @private
 */
ListItemController._refreshAttributesProperties = function() {
    for (var key in this.attrProperties) {
        if (this.attrProperties[key] != undefined) {
            //console.log("createElement: found attrProperty: " + key);
            var data = this.attrProperties[key];
            //console.log("createElement: data");
            //console.log(data);
            var elementID = this.generateElementID(data["idPrefix"]);
            //console.log("createElement: elementID: " + elementID);
            var element = document.getElementById(elementID);
            if (element != null) {
                var value = this.model.getProperty(key);
                element.setAttribute(data["attribute"], value);
            }
        }
    }
};

/**
 * @private
 */
ListItemController._refreshMapAttributes = function() {
    for (var key in this.attrMaps) {
        var data = this.attrMaps[key];
        if (this.model.hasProperty(key)) {
            var elementID = this.generateElementID(data["idPrefix"]);
            //console.log("createElement: elementID: " + elementID);
            var element = document.getElementById(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);
                }
            }
        }
    }
};

/**
 * @private
 */
ListItemController._refreshPropertyMethods = function() {
    for (var key in this.propertyMethods) {
        var elementID, element, i, methodElements, elementName;
        if (this.propertyMethods[key] != undefined) {
            var data = this.propertyMethods[key];
            //console.debug("_refreshPropertyMethods : propertyMethod defined", key, data);

            if (data["idPrefixes"] != undefined) {
                methodElements = {};
                for (i = 0; i < data["idPrefixes"].length; i++) {
                    var elementField = data["idPrefixes"][i];
                    elementID = this.generateElementID(elementField);
                    //console.debug("_refreshPropertyMethods elementID", elementID);
                    element = document.getElementById(elementID);

                    if (element != null) {
                        methodElements[elementField] = element;
                    }
                }

                this[data.method](key, methodElements);
            } else {
                this[data.method](key);
            }
        }
    }
};

/**
 * Reacts to a change of any propert in the model attached to this item.
 */
ListItemController.propertyChangeEvent = function(property, oldValue,
                                                  newValue) {
    // console.log("ListItemController - propertyChangeEvent", 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();
};


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


/**
 *
 * @param model
 */
ListItemController.setModel = function(model) {
    if (this.model){
        this.unsetModel();
    }
    this.model = model;
    this.model.addListener(this);
    if (this.myElement != undefined) {
        this.refresh();
    }
};


/**
 * Create the actual HTML element that will match this list item, including
 * the content from the model set up earlier.
 *
 * 'document' is the document to enter the newly created element into, and
 * 'containerElement' is the element that it will be appended to.
 * If 'beforeElement' is defined, the new element should be created right
 * before it, but as a child of 'containerElement'.
 */
ListItemController.createElement = function(document,
                                            containerElement,
                                            beforeElement) {
    //console.log("createElement - listID: " + this.listID);
    this.IDPrefix = "ListItemController" + this.getListID() + ".";
    var newNode = cloneNodeSetIDs(this.itemTemplate,
                                  this.IDPrefix,
                                  this.getListItemID());

    //console.log("createElement - newNode:", newNode, containerElement);
    //console.debug("container in DOM", document.getElementById(containerElement.id) ? true: false);

    if (beforeElement == undefined) {
        containerElement.appendChild(newNode);
    } else {
        containerElement.insertBefore(newNode, beforeElement);
    }

    this.myElement = newNode;

    if (this.model !== undefined) {
        this.refresh();
    }
    /*
     var nodeID = newNode.id;
     nodeID = nodeID.replace("#n", "#" + this.listID);

     newNode.id = nodeID;
     */

    //console.log("createNewElement done");
};


/**
 * Return the element from within the containing element of this list item
 * that matches 'id'.
 * The original 'id' in the HTML will be of the form "<id>_#n". This is
 * mangled upon drawing of this item and thus getContainedElement() should
 * be used to find it.
 *
 * Will work only after createElement() has been called (probably by the
 * list controller).
 */
ListItemController.getContainedElement = function(id) {
    var element;

    element = containedElementByID(this.myElement, this.getFullElementID(id));

    if (element == undefined) {
        throw "Could not find contained element '" + id + "'.";
    }

    return element;
};

/**
 *
 */
ListItemController.deleted = function() {
    if (this.myElement){
        this.myElement.parentNode.removeChild(this.myElement);
    }
};


/**
 * Return the HTML element that represents this item.
 */
ListItemController.getElement = function() {
    return this.myElement;
};


/*****************************************************************************
 * This is a special ListItemController that manages a sub-list within
 * the item. Handy for doing hierarchical lists or groups of lists
 * (sectioned lists).
 *
 * @class StorkUtil.ListOfListsItem
 * @extends StorkUtil.ListItemController
 * @singleton
 *****************************************************************************/
var ListOfListsItem = clone(ListItemController);

/**
 * @property subListController
 */
ListOfListsItem.clonedProperty("subListController", ListController);


/**
 * Get the ListController for the sub-list within this item.
 * This ListController is automatically created when a ListOfListItem is
 * cloned.
 */
ListOfListsItem.getSubListController = function() {
    return this.subListController;
};


/**
 * Set the ID prefix of the HTML element which will be copied and made
 * the parent element of the sub-list items.
 * 'id' is the prefix. In the document it should appear as <id>_#n, which
 *      will be changed for each copy.
 */
ListOfListsItem.setSubListContainerID = function(id) {
    this.subListContainer = id;
};


/**
 * The name of the StorkModel property for this item which contains a
 * ListModel object as its value, and which will be used to populate the
 * sub-list
 */
ListOfListsItem.setSubListModelProperty = function(propertyName) {
    this.subListModelProperty = propertyName;
};


/**
 * Override the normal ListItemController createElement() with one that
 * builds the sub-list.
 */
ListOfListsItem.createElement = function(document, containerElement,
                                         beforeElement) {
    var subListModelProperty;
    var listContainer;

    //console.debug("ListOfListsItem - createElement");
    this.superMethod(ListOfListsItem.createElement, "createElement", document, containerElement, beforeElement);

    listContainer = this.getContainedElement(this.subListContainer);
    this.subListController.setListContainer(listContainer);

    subListModelProperty = this.model.getProperty(this.subListModelProperty);
    this.subListController.setModel(subListModelProperty);
};


//***********************************************************
// * Model for lists
//**********************************************************/

/**
 * Model for lists
 *
 * @class StorkUtil.ListModel
 * @extends StorkCore.StorkModel
 * @singleton
 */
var ListModel = clone(StorkModel);

/**
 * @property {Array} items
 */
ListModel.copiedProperty("items", []);


/**
 * Return number of elements in this list model.
 */
ListModel.getLength = function() {
    return this.items.length;
};


/**
 * Delete an item from the model.
 *
 * Searches through the list for the given item and removes it from the list.
 * Will call 'deleteItemEvent' method of any listeners, if they have
 * implemented that method. The method will be passed, as arguments, this
 * ListModel, the item deleted, and the index of the item deleted.
 */
ListModel.deleteItem = function(item) {
    var index;

    for (index = 0; index < this.items.length; index++) {
        if (this.items[index] === item) {
            // Found item
            return this.deleteIndex(index);
        }
    }

    throw "Could not find item in list";
};


/**
 * Delete an item from the model by index.
 *
 * Will call 'deleteItemEvent' method of any listeners, if they have
 * implemented that method. The method will be passed, as arguments, this
 * ListModel, the item deleted, and the index of the item deleted.
 */
ListModel.deleteIndex = function(index) {
    var i, item;

    item = this.items[index];
    this.items.splice(index, 1);
    for (i=0; i < this.listeners.length; i++) {
        if (this.listeners[i].deleteItemEvent != undefined) {
            this.listeners[i].deleteItemEvent(this, item, index);
        }
    }
};


/**
 * Insert a list item model object, based on 'object',
 * before the one specified in 'index'.
 *
 * @param {Object} object is any Javascript object. Each property of it will be made
 *      into a StorkModel property.
 *
 *      Calls insertBeforeEvent(sourceModel, model, index) for each listener,
 *      where 'sourceModel' is this ListModel, 'model' is the added model and
 *
 * @param {Number} index is the index where it now resides.
 *
 * @return {StorkCore.StorkModel}
 */

ListModel.insertObjectBefore = function(object, index) {
    var model = StorkModel.createFromObject(object);
    this.items.splice(index, 0, model);
    this.informListeners("insertBeforeEvent", [this, model, index]);
    return model;
};


/**
 * Insert a list item model object, based on 'object' to end of list.
 *
 * @param {Object} object is any Javascript object. Each property of it will
 *          be made into a StorkModel property.
 *
 *          Calls insertBeforeEvent(sourceModel, model, index) for each
 *          listener, where 'sourceModel' is this ListModel, 'model' is the
 *          added model
 *
 * @param {Number} index is the index where it now resides.
 */
ListModel.pushObject = function(object) {
    var model = StorkModel.createFromObject(object);
    this.items.push(model);
    this.informListeners("insertBeforeEvent",
                         [this, model, this.items.length-1]);
    return model;
};


/**
 *
 */
ListModel.informListeners = function(eventName, args) {
    for (var i=0; i < this.listeners.length; i++) {
        if(this.listeners[i][eventName] &&
           typeof this.listeners[i][eventName] === 'function') {
            this.listeners[i][eventName].apply(this.listeners[i], args);
        }
    }
};

/**
 * Build the model for this list from an array of objects.
 * Each object in the array becomes a StorkModel with the properties of
 * the object as model properties.
 *
 * 'items'  - An array of objects.
 */
ListModel.setItemsFromArray = function(items) {
    var model;
    this.items = [];
    for (var i = 0; i <  items.length; i++) {
        model = StorkModel.createFromObject(items[i]);
        this.items[i] = model;
    }
    this.informListeners("resetEvent",
                         [this]);
};


/**
 * Return an array representing each item in this ListModel. Each element in
 * the array will be an object with the ListModel item's properties as direct
 * keys in the object (ie. each StorkModel in the ListModel is flattened
 * into a direct object).
 *
 * This is the reverse of setItemsFromArray().
 */
ListModel.getItemsAsArray = function() {
    var ob, model;
    var items = [];

    for (var i = 0; i < this.items.length; i++) {
        model = this.items[i];
        items.push(model.toJSON());
    }

    return items;
};


/**
 * Get the item model for the given 'itemNumber' (an index into the item list).
 */
ListModel.getItemModel = function(itemNumber) {
    return this.items[itemNumber];
};

/** ***********************************************************************
 * implements a list with filtering of its items
 **************************************************************************/
var FilteredListModel = clone(ListModel);

/**
 * abstract method to determine if an item must be included. The method returns
 * true to include item (in same fashion as django does).
 *
 * @param {Object} item the item to evaluate
 * @return {Boolean} true if the item should be included
 */
FilteredListModel._applyFilter = function(item){
    return this.filterFn(item);
};

FilteredListModel.filterFn = function(item){
    return true;
}

/**
 * takes all the items from a list model and applies filter over them. Makes
 * the items for the filtered list the ones that match the filter
 * function.
 *
 * @param {ListModel} listModel list model to filter
 */
FilteredListModel._setItemsFromListModel = function(listModel){
    var i, len, item;
    // make the filtering
    this.items = [];
    for (i=0,len=listModel.getLength(); i<len; i++){
        item = listModel.getItemModel(i);
        if (this._applyFilter(item)===true){
            this._pushObject(item);
        }
    }
    this.informListeners("resetEvent", [this]);
};

/**
 * sets the list model to filter
 */
FilteredListModel.setListModel = function(listModel){
    var me = this;

    if (this._listModel){
        this.unsetListModel();
    }

    this._listModel = listModel;
    this._setItemsFromListModel(listModel);

    this._listModelListeners = {
        resetEvent:function(listModel){
            me._setItemsFromListModel(listModel);
        },
        insertBeforeEvent: function(listModel, model, index){
            var lastItem=null, newIndex, i, len;
            if (me._applyFilter(model)===true){
                // find the position in the filtered array

                // if the model is added at the end
                if (index == (listModel.getLength() - 1)){
                    me._pushObject(model);
                    return;
                }else if (index <= 0){
                    // if the index points to the first position,
                    // add it at the beginning
                    me._insertObjectBefore(model, 0);
                    return;
                }

                newIndex = index - 1;
                lastItem = listModel.getItemModel(newIndex);
                while (me._applyFilter(lastItem)!==true && newIndex>0){
                    newIndex--;
                    lastItem = listModel.getItemModel(newIndex);
                }

                // if the index points to the first position,
                // add it at the beginning
                if (newIndex === 0){
                    me._insertObjectBefore(model, 0);
                    return;
                }

                // finally find the index in the current array for the found
                // item and add it after it (i + 1).
                for (i=0, len=me.items.length; i<len; i++){
                    if (lastItem == me.items[i]){
                        me._insertObjectBefore(model, i+1);
                        break;
                    }
                }
            }
        },
        deleteItemEvent: function(listModel, model, index){
            var i,len;
            for (i=0, len=me.items.length; i<len; i++){
                if (model == me.items[i]){
                    me._deleteIndex(i);
                    break;
                }
            }
        }
    };
    this._listModel.addListener(this._listModelListeners);
};

FilteredListModel.unsetListModel = function(){
    if (this._listModel){
        this._listModel.removeListener(this._listModelListeners);
        this._listModel = null;
    }
};


//override
FilteredListModel._insertObjectBefore = function(model, index) {
    this.items.splice(index, 0, model);
    this.informListeners("insertBeforeEvent", [this, model, index]);
    return model;
};

//override
FilteredListModel._pushObject = function(model) {
    this.items.push(model);
    this.informListeners("insertBeforeEvent",
                         [this, model, this.items.length-1]);
    return model;
};

FilteredListModel._deleteIndex = FilteredListModel.deleteIndex;

FilteredListModel.throwReadOnlyError = function(){
    throw Error("Method not available in read only FilteredListModel instance");
};
FilteredListModel.pushObject = FilteredListModel.throwReadOnlyError;
FilteredListModel.setItemsFromArray = FilteredListModel.throwReadOnlyError;
FilteredListModel.insertObjectBefore = FilteredListModel.throwReadOnlyError;
FilteredListModel.deleteItem = FilteredListModel.throwReadOnlyError;
FilteredListModel.deleteIndex = FilteredListModel.throwReadOnlyError;


/** ***********************************************************************
 * implements a list with exclusion of its items
 **************************************************************************/
var ExcludedListModel = clone(FilteredListModel);

ExcludedListModel._applyFilter = function(item){
    return !this.excludeFn(item);
};

ExcludedListModel.excludeFn = function(item){
    return true;
};