storkCore

list.js at [3dba570498]
Login

list.js at [3dba570498]

File list.js artifact 8c904649ce part of check-in 3dba570498


/**
 * Controller for list widgets with items
 *
 * @class StorkUtil.ListController
 * @extends StorkCore.StorkController
 * @singleton
 */

// :NOTE: Should look at utilising StorkHtmlView here and away from the
// controller creating HTML

var ListController = clone(StorkController);

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

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

// ListController.textContentFrom = 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;
    this.refresh();
};


/**
 * 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;
    this.refresh();
};

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.
 *
 * 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!";
    }*/

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

    this.model = listModel;
    listModel.removeListener(this);
    this.refresh();
    listModel.addListener(this);
};


ListController.refresh = function() {
    if (this.model && this.containerElement && this.itemControllerProto) {
        var listModel = this.model;
    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(this.containerElement);
        this.itemControllers[i] = itemController;
        this.informListeners('listItemControllerAdded',[this,
                                                        itemController,
                                                        i]);
    }
}

    //return this.superMethod(ListController.refresh, "refresh");
};


/**
 * 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) {
    if (! (this.model && this.containerElement && this.itemControllerProto) ) {
        // We only care for these events after the model, container element 
        // and item controller prototype
        // are set. Ie. after HTML has been created for the list items.
        return;
    }

    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);*/
    //console.debug(source);
    console.debug(modelItem);
    if (! (this.model && this.containerElement && this.itemControllerProto) ) {
        return;
    }
    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(this.containerElement, nextElement);
        this.itemControllers.splice(index, 0, itemController);
    } else {
        /*console.log(this.containerElement);*/
        itemController.createElement(this.containerElement);
        this.itemControllers.push(itemController);
    }

    itemController.setListModel(this.model);

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

    this.showOrHideZeroLengthElement();
};


/*****************************************************************************
 * Controller for items contained within lists
 *****************************************************************************/
var ListItemController = clone(StorkController);

/**
 * @property itemTemplate
 */
ListItemController.itemTemplate = undefined;
ListItemController.templateProperty = 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", {});
ListItemController.copiedProperty("styleProperties", {});
/**
 * @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;
};


/**
 * 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;
    // Calling setItemTemplate overrides the template linkage to a
    // model property
    this.templateProperty = undefined;
};


/**
 * Set a property from the model which will select which HTML element
 * to use as a template for this item when rendering. When rendering the
 * HTML element will be searched by its ID using a combination of 
 * prefix+value-of-property. Setting the template explicitly with
 * setItemTemplate() will override this.
 *
 * 'propertyName' is the model property to use to select a template.
 * 'prefix', if set, gives a prefix for the HTML element's ID when fetching.
 */

ListItemController.templateFromProperty = function(propertyName, prefix) {
    this.templateProperty = {
        propertyName: propertyName,
        prefix: prefix
    };
};


/**
 * Links the 'modelProperty' property from the model to the text content of the
 * HTML element for this list item.
 * Note that this overrides all other content from the element (any other
 * content from the HTML template for this list item), and thus nullifies
 * things like linkTextProperty(). If this is not desired, linkTextProperty()
 * should be used instead and linked to a suitable HTML element (e.g.
 * a <span>).
 */

ListItemController.linkTextContent = function(modelProperty) {
    this.textContentProperty = modelProperty;
};


/**
 * 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
    };
};


/**
 * Links a property of the model of this list item to a HTML style property
 * in the rendered result, from the element with the ID prefixed 'idPrefix',
 * and ending in "_#n".
 *
 * 'styleName' is the style property which will be changed to the value given
 *             in the equivalent model.
 */

ListItemController.linkStyleProperty = function(modelProperty, idPrefix,
                                                styleName) {
    console.debug("called");
    this.styleProperties[modelProperty] = {
        idPrefix: idPrefix,
        style: styleName
    };
};


/**
 * 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 exist in this
 * ListItemController.
 *
 * 'elementIDs', if set, is an array 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.
 *               In this array the IDs should be given without the "_#n" 
 *               suffix.
 *
 *               The actual elements will be looked up and passed to the method
 *               in its 'elements' argument as a dictionary with the given
 *               ID as the key, and the element as a value, such as:
 *               {givenID: <element>, anotherID: <anotherElement>}
 *
 *               If this is not defined, '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 ( (this.templateProperty != undefined) && (this.model) ) {
            // Template to be fetched based on model property
            if ( (this.templateProperty.prefix != undefined) &&
                 (this.templateProperty.prefix != null)) {
                let prefix = this.templateProperty.prefix;
            } else {
                let prefix = "";
            }
            let id = prefix +
                this.model.getProperty(this.templateProperty.property);
            let this.myElement = elementByID(id);
            if (! this.myElement) {
                // Still can't find element so don't do anything.
                return;
            }
        } else {
            // If the element hasn't been set yet, do nothing.
            return;
            }*/
        return;
    }
    
    //console.debug("refresh", this.myElement);
    this._refreshTextProperties();
    this._refreshTextPropertyMethods();
    this._refreshAttributesProperties();
    this._refreshStyleProperties();
    this._refreshMapAttributes();
    this._refreshPropertyMethods();
    this._refreshTextContent();
};


ListItemController._refreshTextContent = function() {
    if ( (this.textContentProperty != undefined) &&
         (this.myElement != undefined)) {
        this.myElement.textContent = 
            this.model.getProperty(this.textContentProperty);
    }
};


/**
 * @private
 */
ListItemController._refreshTextProperties = function() {
    for (var key in this.textProperties) {
        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(elementID);
            var textContainer = document.getElementById(elementID);
            if (textContainer != null) {
                emptyElement(textContainer);
                var text = this.model.getProperty(key);
                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) {
            var data = this.attrProperties[key];
            //console.log(data);
            var elementID = this.generateElementID(data["idPrefix"]);
            var element = document.getElementById(elementID);
            if (element != null) {
                var value = this.model.getProperty(key);
                element.setAttribute(data["attribute"], value);
            }
        }
    }
};

ListItemController._refreshStyleProperties = function() {
    for (var key in this.styleProperties) {
        if (this.styleProperties[key] != undefined) {
            var data = this.styleProperties[key];
            console.debug(data);
            var elementID = this.generateElementID(data["idPrefix"]);
            console.debug(elementID);
            var element = document.getElementById(elementID);
            if (element != null) {
                console.debug("element not null");
                var value = this.model.getProperty(key);
                console.debug(key);
                console.debug(value);
                element.style[data["style"]] = 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"]);
            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 property in the model attached to this item.
 */
ListItemController.propertyChangeEvent = function(model, property, oldValue,
                                                  newValue) {
    console.debug(property);
    if (this.templateProperty &&
        (property == this.templateProperty.propertyName)) {
        // Property that was changed in the model is the one
        // that affects which template to use for rendering. 
        // We need to recreate the element based on the new template.
        if (!this.myElement) {
            // An element for this item hasn't yet been created, which means
            // createElement wasn't yet called, so we shouldn't do create
            // it ourselves
        } else {
            this.createElement(this.myElement.parentNode);
            return
        }
    }
    // 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. This is mostly intended to be
 * called by a ListController.
 *
 * 'containerElement' is the element that the newly created element 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(containerElement,
                                            beforeElement) {
    if (this.myElement) {
        // Previous element was created, remove that and recreate in same place
        if (!beforeElement) {
            beforeElement = this.myElement.nextSibling;
        }
        this.myElement.parentNode.removeChild(this.myElement);
        this.itemTemplate = null;
    }
    
    this.IDPrefix = "ListItemController" + this.getListID() + ".";

    if (!this.itemTemplate) {
        // No template set, maybe there is a model property to look at
        // that is linked to a template?
        if ( (this.templateProperty != undefined) && (this.model) ) {
            // Template to be fetched based on model property
            let prefix = "";
            if ( (this.templateProperty.prefix != undefined) &&
                 (this.templateProperty.prefix != null)) {
                prefix = this.templateProperty.prefix;
            }
            console.debug(this.templateProperty);
            let id = prefix +
                this.model.getProperty(this.templateProperty.propertyName) +
                "_#n";
            console.debug(id);
            this.itemTemplate = elementByID(id);

        }
    }

    if (! this.itemTemplate) {
        // Still can't find element, not good
        throw "ListItemController trying to create element but no template given"
    }
    
    var newNode = cloneNodeSetIDs(this.itemTemplate,
                                  this.IDPrefix,
                                  this.getListItemID());

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

    this.myElement = newNode;

    if (this.model !== undefined) {
        this.refresh();
    }

    //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);

ListOfListsItem.renderIfEmpty = true;


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


ListOfListsItem.deleted = function() {
    this.superMethod(ListOfListsItem.deleted, "deleted");

    if (this.model && this.sublistModelProperty) {
        subListModel = this.model.getProperty(this.subListModelProperty);
        sublistModel.removeListener(this);
    }
    return;
};

    
/**
 * 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;
    ListOfListsItem.setupSubListModel();
};


/**
 * Override ListItemController refresh() to implement functionality of
 * renderIfEmpty().
 */

ListOfListsItem.refresh = function() { 
    var r = this.superMethod(ListOfListsItem.refresh, "refresh");

    if (! this.renderIfEmpty) {
        var sublistModel = this.getSubListController().getModel();

        if (!sublistModel || (sublistModel.getLength() == 0)) {
            hideElement(this.myElement);
        } else {
            showElement(this.myElement);
        }                
    }        

    return r;
};


/* Setup the model of the sublist, and set ourselves as a listener of it.
   This only does something if a model has been set for this item, and
   the subListModelProperty has been set. 

   Intended for internal use.
*/

ListOfListsItem.setupSubListModel = function() {
    if (this.subListModelProperty && this.model) {
        var sublistModel = this.model.getProperty(this.subListModelProperty);
        var oldModel = this.subListController.getModel();
        if (oldModel) {
            oldModel.removeListener(this);
        }
        this.subListController.setModel(sublistModel);
        sublistModel.addListener(this);
    }
};


ListOfListsItem.setModel = function(listModel) {
    var r = this.superMethod(ListOfListsItem.setModel, "setModel", listModel);

    this.setupSubListModel();

    return r;
};


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

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

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

    /*
    subListModel = this.model.getProperty(this.subListModelProperty);
    console.log("createElement setModel");
    this.subListController.setModel(subListModel);
    console.log("createElement addListener");
    subListModel.addListener(this);
    console.log("createElement return");
*/
    this.refresh();
};


ListOfListsItem.renderIfEmpty = function(render) {
    this.renderIfEmpty = render;
    return;
};


/* If the underlining list changes, refresh this item. The item may need to 
   change its appearance based on changes in the sublist */

ListOfListsItem.insertBeforeEvent = function(list, item, index) {
    this.refresh();
};

ListOfListsItem.deleteItemEvent = function(list, item, index) {
    this.refresh();
};

ListOfListsItem.resetEvent = function(list) {
    this.refresh();
};

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

/**
 * Model for lists
 *
 * Events thrown: 
 * insertBeforeEvent  (newItem, index)
 * deleteItemEvent (item, index)
 * resetEvent
 */
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);
    this.informListeners("deleteItemEvent", [this, item, index]);
    /*
    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
 *      '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 into the list.
 *
 *          Calls insertBeforeEvent(sourceModel, model, index) for each
 *          listener, where 'sourceModel' is this ListModel, 'model' is the
 *          added model
 */

ListModel.push = function(storkModel) {
    if (! storkModel) {
        throw("Tried to push an undefined or null StorkModel object to a ListModel");
    }
    this.items.push(storkModel);
    this.informListeners("insertBeforeEvent",
                         [this, storkModel, this.items.length]);
    return;
};


/**
 * 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
 *
 * @return Return created StorkModel object from 'object'.
 */

ListModel.pushObject = function(object) {
    var model = StorkModel.createFromObject(object);
    this.push(model);
    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);
        }
    }
};


/**
 * Empty the list.
 */

ListModel.empty = function() {
    this.items = [];
    this.informListeners("resetEvent", [this]);
};


/**
 * 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 'index' (an index into the item list).
 */
ListModel.getItemModel = function(index) {
    return this.items[index];
};

/** ***********************************************************************
 * 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]);
    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;
};