    /**
     * @class YAHOO.ext.grid.XMLDataModel
     * This is an implementation of a DataModel used by the Grid. It works
     * with XML data.
     * <br>Example schema from Amazon search:
     * <pre><code>
     * var schema = {
     *     tagName: 'Item',
     *     id: 'ASIN',
     *     fields: ['Author', 'Title', 'Manufacturer', 'ProductGroup']
     * };
     * </code></pre>
     * @extends YAHOO.ext.grid.LoadableDataModel
     * @constructor
     * @param {Object} schema The schema to use
     * @param {XMLDocument} xml An XML document to load immediately
    */
    YAHOO.ext.grid.XMLDataModel = function(schema, xml){
        YAHOO.ext.grid.XMLDataModel.superclass.constructor.call(this, YAHOO.ext.grid.LoadableDataModel.XML);
        /**@private*/
        this.schema = schema;
        this.xml = xml;
        if(xml){
            this.loadData(xml);
        }
        this.idSeed = 0;
    };
    YAHOO.extendX(YAHOO.ext.grid.XMLDataModel, YAHOO.ext.grid.LoadableDataModel, {

        getDocument: function(){
           return this.xml;
        },

        /**
         * Overrides loadData in LoadableDataModel to process XML
         * @param {XMLDocument} doc The document to load
         * @param {<i>Function</i>} callback (optional) callback to call when loading is complete
         * @param {<i>Boolean</i>} keepExisting (optional) true to keep existing data
         * @param {<i>Number</i>} insertIndex (optional) if present, loaded data is inserted at the specified index instead of overwriting existing data
         */
        loadData: function(doc, callback, keepExisting, insertIndex){
        	this.xml = doc;
        	var idField = this.schema.id;
        	var fields = this.schema.fields;
        	if(this.schema.totalTag){
        	    this.totalCount = null;
        	    var totalNode = doc.getElementsByTagName(this.schema.totalTag);
        	    if(totalNode && totalNode.item(0) && totalNode.item(0).firstChild) {
                    var v = parseInt(totalNode.item(0).firstChild.nodeValue, 10);
                    if(!isNaN(v)){
                        this.totalCount = v;
                    }
            	}
        	}
        	var rowData = [];
        	var nodes = doc.getElementsByTagName(this.schema.tagName);
            if(nodes && nodes.length > 0) {
        	    for(var i = 0; i < nodes.length; i++) {
        	        var node = nodes.item(i);
        	        var colData = [];
        	        colData.node = node;
        	        colData.id = this.getNamedValue(node, idField, String(++this.idSeed));
        	        for(var j = 0; j < fields.length; j++) {
        	            var val = this.getNamedValue(node, fields[j], "");
        	            if(this.preprocessors[j]){
        	                val = this.preprocessors[j](val);
        	            }
        	            colData.push(val);
        	        }
        	        rowData.push(colData);
        	    }
            }
            if(keepExisting !== true){
               YAHOO.ext.grid.XMLDataModel.superclass.removeAll.call(this);
        	}
        	if(typeof insertIndex != 'number'){
        	    insertIndex = this.getRowCount();
        	}
            YAHOO.ext.grid.XMLDataModel.superclass.insertRows.call(this, insertIndex, rowData);
            if(typeof callback == 'function'){
            	callback(this, true);
            }
            this.fireLoadEvent();
        },

        /**
         * Adds a row to this DataModel and syncs the XML document
         * @param {String} id The id of the row, if null the next row index is used
         * @param {Array} cellValues The cell values for this row
         * @return {Number} The index of the new row (if the model is sorted this index may not be accurate)
         */
        addRow: function(id, cellValues){
            var node = this.createNode(this.xml, id, cellValues);
            cellValues.id = id || ++this.idSeed;
            cellValues.node = node;
            return YAHOO.ext.grid.XMLDataModel.superclass.addRow.call(this, cellValues);
        },

        /**
         * Inserts a row into this DataModel and syncs the XML document
         * @param {Number} index The index to insert the row
         * @param {String} id The id of the row, if null the next row index is used
         * @param {Array} cellValues The cell values for this row
         * @return {Number} The index of the new row (if the model is sorted this index may not be accurate)
         */
        insertRow: function(index, id, cellValues){
            var node = this.createNode(this.xml, id, cellValues);
            cellValues.id = id || ++this.idSeed;
            cellValues.node = node;
            return YAHOO.ext.grid.XMLDataModel.superclass.insertRow.call(this, index, cellValues);
        },

        /**
         * Removes the row from DataModel and syncs the XML document
         * @param {Number} index The index of the row to remove
         */
        removeRow: function(index){
            var node = this.data[index].node;
            node.parentNode.removeChild(node);
            YAHOO.ext.grid.XMLDataModel.superclass.removeRow.call(this, index, index);
        },

        getNode: function(rowIndex){
            return this.data[rowIndex].node;
        },

        /**
         * Override this method to define your own node creation routine for when new rows are added.
         * By default this method clones the first node and sets the column values in the newly cloned node.
         * In many instances this will not work and you will have to create the node manually.
         * @param {XMLDocument} xmlDoc The xml document being used by this model
         * @param {String/Number} id The row id
         * @param {Array} colData The column data for the new node
         * @return {XMLNode} The created node
         */
        createNode: function(xmlDoc, id, colData){
            var template = this.data[0].node;
            var newNode = template.cloneNode(true);
            var fields = this.schema.fields;
            for(var i = 0, len = fields.length; i < len; i++){
                var nodeValue = colData[i];
                if(this.postprocessors[i]){
                    nodeValue = this.postprocessors[i](nodeValue);
                }
                this.setNamedValue(newNode, fields[i], nodeValue);
            }
            if(id){
                this.setNamedValue(newNode, this.schema.idField, id);
            }
            template.parentNode.appendChild(newNode);
            return newNode;
        },

        /**
         * @private
         * Convenience function looks for value in attributes, then in children tags - also
         * normalizes namespace matches (ie matches ns:tag, FireFox matches tag and not ns:tag).
         */
        getNamedValue: function(node, name, defaultValue){
        	if(!node || !name){
        		return defaultValue;
        	}
        	var nodeValue = defaultValue;
            var attrNode = node.attributes.getNamedItem(name);
            if(attrNode) {
            	nodeValue = attrNode.value;
            } else {
                var childNode = node.getElementsByTagName(name);
                if(childNode && childNode.item(0) && childNode.item(0).firstChild) {
                    nodeValue = childNode.item(0).firstChild.nodeValue;
            	}else{
            	    // try to strip namespace for FireFox
            	    var index = name.indexOf(':');
            	    if(index > 0){
            	        return this.getNamedValue(node, name.substr(index+1), defaultValue);
            	    }
            	}
            }
            return nodeValue;
        },

        /**
         * @private
         * Convenience function set a value in the underlying xml node.
         */
        setNamedValue: function(node, name, value){
        	if(!node || !name){
        		return;
        	}
        	var attrNode = node.attributes.getNamedItem(name);
            if(attrNode) {
            	attrNode.value = value;
            	return;
            }
            var childNode = node.getElementsByTagName(name);
            if(childNode && childNode.item(0) && childNode.item(0).firstChild) {
                childNode.item(0).firstChild.nodeValue = value;
            }else{
        	    // try to strip namespace for FireFox
        	    var index = name.indexOf(':');
        	    if(index > 0){
        	        this.setNamedValue(node, name.substr(index+1), value);
        	    }
        	}
        },

        /**
         * Overrides DefaultDataModel.setValueAt to update the underlying XML Document
         * @param {Object} value The new value
         * @param {Number} rowIndex
         * @param {Number} colIndex
         */
        setValueAt: function(value, rowIndex, colIndex){
            var node = this.data[rowIndex].node;
            if(node){
                var nodeValue = value;
                if(this.postprocessors[colIndex]){
                    nodeValue = this.postprocessors[colIndex](value);
                }
                this.setNamedValue(node, this.schema.fields[colIndex], nodeValue);
            }
            YAHOO.ext.grid.XMLDataModel.superclass.setValueAt.call(this, value, rowIndex, colIndex);
        },

        /**
         * Overrides getRowId in DefaultDataModel to return the ID value of the specified node.
         * @param {Number} rowIndex
         * @return {Number}
         */
        getRowId: function(rowIndex){
            return this.data[rowIndex].id;
        },

        addRows : function(rowData){
            for(var j = 0, len = rowData.length; j < len; j++){
               var cellValues = rowData[j];
               var id = ++this.idSeed;
               var node = this.createNode(this.xml, id, cellValues);
               cellValues.node=node;
               cellValues.id = cellValues.id || id;
               YAHOO.ext.grid.XMLDataModel.superclass.addRow.call(this,cellValues);
            }
        },

       insertRows : function(index, rowData){
           // copy original array so it is not reversed
           rowData = rowData.slice(0).reverse();
           for(var j = 0, len = rowData.length; j < len; j++){
              var cellValues = rowData[j];
              var id = ++this.idSeed;
              var node = this.createNode(this.xml, id, cellValues);
              cellValues.id = cellValues.id || id;
              cellValues.node = node;
              YAHOO.ext.grid.XMLDataModel.superclass.insertRow.call(this, index, cellValues);
           }
       }
    });

Copyright © 2006 Jack Slocum. All rights reserved.