Sebastian Werner schrieb:
Hi Christian,

please explain what are your exact use cases. Do you just add properties e.g. IDs etc. or do you even modify them? To you attach modifiers? Do you do this on instance level or on prototype level?

Cheers,

Sebastian

Hi Sebastian, have a look at the attached file. It is documented to be self-explaining. As you will see, the whole idea depends on the add-properties-at-runtime concept. I add behaviour to all widgets by adding to the QxTarget and QxWidget prototypes, and then overriding those behaviours on a per-widget basis also to their prototypes.

Thanks,

Christian

/**
 * Qoodoo Data Manager Extensions
 * 
 * @author Christian Boulanger
 * @version 0.1
 * @copyright    Christian Boulanger
 * @license      GNU Lesser General Public License, see 
http://www.opensource.org/licenses/lgpl-license.php 
 *
 * With these extensions, all Objects inheriting from QxTarget get data binding 
 * and, if they have properties that implement "values," "states," or 
"options", 
 * these properties can be bound to data sources on the server
 * 
 * The data transmitted to or from the server is always encoded in a simple 
JSON 
 * object with this structure
 * { 
 *        data: (mixed data),
 *        options: (mixed options),
 *        (any kind of property): (property value),
 *    ...
 * }
 * and sent by the chosen transport mechanism and method.
 * 
 * In most widgets, the options property can be a string ( list values 
separated by 
 * comma), an array with string values, or a hash with "text" and "value" 
properties for
 * the labels and the values of widgets which support this difference (for 
example,
 * QxListElements).
 * 
 * The difference between "data" and "options" can be a bit confusing:
 * - In a QxList, the "options" are the QxListViewItems and the "data"  are the 
values of 
 *   the items selected (obviously, for this to work, valus must be unique)
 *
 * - In a QxListView, the "options" are the actual Grid data, whereas the
 *   "data" is a list of id numbers of the selected rows. 
 *
 * You can also dynamically change the datasource on the server by setting a new
 * reiceveUrl or sendUrl property, or set whatever property you like with the 
JSON
 * object.
 * 
 * The following properties are added to QxTarget and its descendents:
 * 
 * Data binding
 * ------------
 * dataManager                                  associates data element and the 
data manager. Object
 * name                                                 name of the data 
element, must be unique
 * method                                               determines whether the 
data should be sent received or sent 
 *                                                              as "get" or 
"post" (default). string.
 * sendUrl                                              url where to fetch the 
data remotely. %foo is replaced by the 
 *                                                              value of 
element with name foo. string.
 * receiveUrl                                   url with which to update data 
remotely. %foo is replaced by the 
 *                                                              value of 
element with name foo. string.
 * 
 * Data events
 * -----------
 * dispatchOnDataSent                   event to dispatch when server has 
acknowledged the receipt of 
 *                                                              the data sent. 
string.
 * dispatchOnDataReceived               event to dispatch globally when server 
has sent data. string.
 * updateRemoteOnChange                 whether to automatically update 
remotely when local data changes. boolean, 
 *                                                              default false.
 * updateRemoteOnEvent                  event name which triggers remote 
update. string.
 * updateLocalOnEvent                   event name which triggers local update. 
string.
 * dispatchOnChange                             event to dispatch when data has 
changed on the client. event name 
 *                                                              will be 
dispatched as a data event containing the changed data
 * 
 * UI events
 * ---------
 * enableOnEvent                                global event to listen for for 
enabling or diabling the element
 * displayOnEvent                               global event to listen for for 
displaying or hiding the element
 * 
 * The data elements are bound together by a QxDataManager object (see below), 
which 
 * provides the transport mechanism. It also has these extended properties and 
can update 
 * all bound data elements centrally.
 * 
 * Drag & Drop support
 * -------------------
 * 
 * The Data Manager Extensions also adds a simple drag & drop default behaviour 
that can be 
 * turned on with the following properties:
 * 
 * dragDataType                                 (string) makes a widget 
draggable and specifies what kind of 
 *                              data type it contains. 
 * childrenDragDataType                 (string) same as dragDataType, but not 
the widget itself is
 *                              made draggable, but its children. this is 
helpful, for example,
 *                                                      in a QxList when you 
don't want to manually assign the dragDataType
 *                              to the QxListItems
 * dropDataType                                 (string) specifies which data 
types are allowed to be dropped 
 *                                                              on the target. 
Can be a list of values separated by comma.
 * dropMarker                                   (object) a widget that shoud 
appear on top of the widget that 
 *                                                              the drag cursor 
currently hovers over, marking the position 
 *                                                              where the 
dragged element will be placed at upon drop.
 * sortAfterDrop                                (boolean) Only applies to 
QxList: should the list be sorted after
 *                                                              dropping? 
 */

//--------------------------------------------------------------------------
// QxTarget Property Extensions
//--------------------------------------------------------------------------

// associates data element and the data container
QxTarget.addProperty({ name : "dataManager", type : QxConst.TYPEOF_OBJECT, 
defaultValue : null});

// name of data element, must be unique
QxTarget.addProperty({ name : "name", type : QxConst.TYPEOF_STRING, 
defaultValue : "" });

// determines whether the data should be sent received or sent as GET or POST
QxTarget.addProperty({ name : "method", type : QxConst.TYPEOF_STRING, 
defaultValue : "post" });

// url where to fetch the data remotely
QxTarget.addProperty({ name : "sendUrl", type : QxConst.TYPEOF_STRING, 
defaultValue : "" });

// url with which to update data remotely
QxTarget.addProperty({ name : "receiveUrl", type : QxConst.TYPEOF_STRING, 
defaultValue : "" });

// event to dispatch when server has acknowledged the receipt of the data sent
QxTarget.addProperty({ name : "dispatchOnDataSent", type : 
QxConst.TYPEOF_STRING, defaultValue : "" });

// event to dispatch globally when server has sent data
QxTarget.addProperty({ name : "dispatchOnDataReceived", type : 
QxConst.TYPEOF_STRING, defaultValue : "" });

// whether to update remotely when local data changes
QxTarget.addProperty({ name : "updateRemoteOnChange", type : 
QxConst.TYPEOF_BOOLEAN, defaultValue : false });

// event name which triggers remote update
QxTarget.addProperty({ name : "updateRemoteOnEvent", type : 
QxConst.TYPEOF_STRING, defaultValue : "" });

// event name which triggers local update
QxTarget.addProperty({ name : "updateLocalOnEvent", type : 
QxConst.TYPEOF_STRING, defaultValue : "" });

// event to dispatch when data has changed on the client
QxTarget.addProperty({ name : "dispatchOnChange", type : QxConst.TYPEOF_STRING, 
defaultValue : ""});


//--------------------------------------------------------------------------
// QxWidget Property Extensions
//--------------------------------------------------------------------------

// global event to listen for for enabling or diabling the element
QxWidget.addProperty({ name : "enableOnEvent", type : QxConst.TYPEOF_STRING, 
defaultValue : ""});

// global event to listen for for displaying or hiding the element
QxWidget.addProperty({ name : "displayOnEvent", type : QxConst.TYPEOF_STRING, 
defaultValue : ""});

// drag and drop property: drop data type
QxWidget.addProperty({ name : "dragDataType", type : QxConst.TYPEOF_STRING, 
defaultValue : ""});

// drag and drop property: drop data type for all children
QxWidget.addProperty({ name : "childrenDragDataType", type : 
QxConst.TYPEOF_STRING, defaultValue : ""});

// drag and drop property: drop data action
QxWidget.addProperty({ name : "dragDataAction", type : QxConst.TYPEOF_STRING, 
defaultValue : ""});

// drag and drop property: drop data action for all children
QxWidget.addProperty({ name : "childrenDragDataAction", type : 
QxConst.TYPEOF_STRING, defaultValue : ""});

// drag and drop property: sort parent after child element has been dragged / 
dropped?
QxWidget.addProperty({ name : "sortAfterDrop", type : QxConst.TYPEOF_BOOLEAN, 
defaultValue : false });

// drag and drop property: marker object
QxWidget.addProperty({ name : "dropMarker", type : QxConst.TYPEOF_OBJECT, 
defaultValue : null});

//--------------------------------------------------------------------------
// Modifiers
//--------------------------------------------------------------------------

/**
 * associates data element with data manager
 */
QxTarget.prototype._modifyDataManager = function( propValue, propOldValue, 
propData )
{
        if ( propValue )
        {
                        propValue.add ( this );
        }
        else if ( propOldValue )
        {
                        propOldValue.remove ( this );   
        };
        return true;
};


/**
 * whether to send data when local data changes
 */
QxTarget.prototype._modifyUpdateRemoteOnChange = function(propValue, 
propOldValue, propData)
{
        var changeEvent = "change" + this.getValuePropName();
                
        if ( propValue )
        {
                this.addEventListener ( changeEvent, this.updateRemote ,this ); 
        } 
        else if ( propOldValue )
        {
                this.removeEventListener ( changeEvent, this.updateRemote ,this 
); 
        };
        return true;
};


/**
 * listens for an event to trigger remote update
 */
QxTarget.prototype._modifyUpdateRemoteOnEvent = function(propValue, 
propOldValue, propData)
{
        if ( propValue )
        {
                window.application.addEventListener (propValue, function(e) {
                        this.updateRemote();
                },this); 
        };
        return true;
};

/**
 * listens for an event to trigger local update
 */
QxTarget.prototype._modifyUpdateLocalOnEvent = function(propValue, 
propOldValue, propData)
{
        if ( propValue )
        {
                window.application.addEventListener (propValue, function(e) {
                        this.updateLocal();
                },this); 
        };
        return true;
};

/**
 * listen for change event event and dispatch passed event name
 */
QxTarget.prototype._modifyDispatchOnChange = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                var changeProp = this.getValuePropName();
                
                this.addEventListener ( "change" + changeProp, function() {
                        this.getDataManager().dispatch ( propValue, 
this.getLocalData() );
                },this); 
        }; 
        return true;
};

/**
 * listen for global event to enable or disable widget
 */
QxTarget.prototype._modifyEnableOnEvent = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                window.application.addEventListener ( propValue, function(e) {
                        this.setEnabled( e.getData() !== false );
                },this); 
        }; 
        return true;
};

/**
 * listen for global event to show or hide widget
 */
QxTarget.prototype._modifyDisplayOnEvent = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                window.application.addEventListener ( propValue, function(e) {
                        this.setDisplay( e.getData() !== false );
                },this); 
        }; 
        return true;
};


//--------------------------------------------------------------------------
// Helper functions
//--------------------------------------------------------------------------

// generic methods suitable for widgets which have 
// getValue and setValue alike methods
// need to be overridden with widget-specific methods
/**
 * gets the name of the value property 
 **/
QxTarget.prototype.getValuePropName = function ()
{
        switch( this.classname )
        {
                case "QxTextField":
                case "QxPasswordField":
                case "QxTextArea":
                        return "Value";
                        
                case "QxLabel":
                        return "Html";
                        
                case "QxText":
                        return "Text";
                        
                case "QxAtom":
                        return "Label";
                                
                default:
                        throw new Error ("getValuePropName: No value property 
defined for " + this.classname + ".");
        };
};

/**
 * gets the setter function
 */
QxTarget.prototype.getSetter = function ()
{
        var setter = "set" + this.getValuePropName();
        return setter;
};

/**
 * gets the getter function
 */
QxTarget.prototype.getGetter = function ()
{
        var getter = "get" + this.getValuePropName();
        return getter;
};

/**
 * gets the url for sending or receiving 
 * dynamically modifies placeholders with data from other fields of the form
 * %foo is replaced by the value of the data element named "foo". This way, you
 * can populate form elements depending on the data of other form elements.
 * @return string url 
 */
QxTarget.prototype._prepareUrl = function( action )
{
        var getter      = 'get' + action.substr(0,1).toUpperCase() + 
action.substr(1) + 'Url',
                url     = typeof this[getter] == "function" ? this[getter](): 
null,
                manager = this.getDataManager(); 
                
        if ( url === null ){
                this.error ("No getter " + getter );
        }
        
        if ( ! url || ! manager ) return false; // do not throw an error since 
this could be intended
        
        // dynamcially modify placeholders
        url = url.replace(/%([a-zA-Z0-9_\-]+)/g,
        function (str, p1) {
                var data = manager.getFieldData(p1);
            if ( typeof data == "undefined" )
            {
                return "*** " + p1 + " is not defined ***";
            } 
            else
            {
                return data;
            }
                }
    ).replace(/%/g,'').replace(/\/\//g,"&"); 
    return url;
}



//--------------------------------------------------------------------------
// Drag & Drop behaviour, optimized for QxList / QxListItems, override for 
// other widgets if necessary
//--------------------------------------------------------------------------

/**
 * dropDataType property modifier
 * enable dragdrop for specific drop data types
 * support for string values in dropDataTypes property, multiple values
 * are separated by comma
 */
QxWidget.prototype._modifyDropDataTypes = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                if ( typeof propValue == "string" )
                {
                        this.setDropDataTypes( propValue.split(",") );
                }
                else
                {
                        this.addEventListener("dragdrop", this.handleDragDrop, 
this );
                        this.addEventListener("dragmove", this.handleDragMove, 
this );
                        this.addEventListener("dragend", this.handleDragEnd, 
this );
                };
        }
        else if ( propOldValue )
        {
                this.removeEventListener ("dragdrop", this.handleDragDrop, this 
);
                this.removeEventListener("dragmove", this.handleDragMove, this 
);
                this.removeEventListener("dragend", this.handleDragEnd, this );
        }; 
        return true;
};

/**
 * dragDataType property modifier:
 * enable dragstart and provide data type
 */
QxWidget.prototype._modifyDragDataType = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                this.addEventListener("dragstart", this.handleDragStart, this );
        }
        else if ( propOldValue )
        {
                this.removeEventListener ("dragstart", this.handleDragStart, 
this );
        }; 
        return true;
};

/**
 * childrenDragDataType property modifier:
 * enable dragstart and provide data type for widget children
 * needs to wait until item get rendered
 */
QxWidget.prototype._modifyChildrenDragDataType = function ( propValue, 
propOldValue, propData )
{
        var self = this;
        window.setTimeout( function()
        {
                var i, children = self.getChildren();
                for ( var i=0; i < children.length; i++ )
                {
                        if ( propValue )
                        {
                                children[i].addEventListener("dragstart", 
self.handleDragStart, children[i] );
                        }
                        else if ( propOldValue )
                        {
                                children[i].removeEventListener ("dragstart", 
self.handleDragStart, children[i] );
                        };              
                };
        }, 1000);
        return true;
};

/**
 * default drag start behaviour
 */             
QxWidget.prototype.handleDragStart = function ( e ) 
{
        var vTarget = e.getCurrentTarget(),
                vType = vTarget.getDragDataType() || 
vTarget.getParent().getChildrenDragDataType(),
                vAction = vTarget.getDragDataAction() || 
vTarget.getParent().getChildrenDragDataAction();
        
        // pevent multiple drag starts 
        if ( vTarget._isBeingDragged ) return;
        vTarget._isBeingDragged = true;
        
        // prevent multiple selection during drag
        if( vTarget.getParent().getManager ) {
                vTarget.getParent().getManager().setDragSelection(false);
        };
        
        if ( vType ){
                e.addData( vType, vTarget );
                e.addAction( vAction || "move" );
                e.startDrag();
        };
};

// internal pointer to currently hovered node
QxWidget.prototype._insertBefore = null;

/**
 * default drag drop behaviour: place item before targeted 
 * item if item allows it
 */
QxWidget.prototype.handleDragDrop = function (e)
{
        e.stopPropagation();    
        var vTarget             = e.getTarget(),
                vType               = e.getDropDataTypes()[0],
                vSource             = e.getData(vType),
                vInsertBefore       = vTarget._insertBefore
                vMarker                         = vTarget.getDropMarker();
        
        // remove marker line
        if ( vMarker ) 
        {
                vMarker.setVisibility(false);
        };

        // move items
        if ( vSource.getParent() && vSource.getParent().getManager )
        {
                // move multiple items
                var vItems = 
vSource.getParent().getManager().getSelectedItems();
                vSource.getParent().getManager().deselectAll();
                for ( var i=0; i <vItems.length; i++ )
                {
                        if ( vInsertBefore && 
vTarget.getParent().contains(vInsertBefore) ) 
                        {
                                vTarget.addBefore ( vItems[i], vInsertBefore );
                        } 
                        else 
                        {
                                vTarget.add ( vItems[i] );
                        };                                              
                };
        }
        else 
        {
                // move sigle item
                if ( vInsertBefore && 
vTarget.getParent().contains(vInsertBefore) ) 
                {
                        vTarget.addBefore ( vSource, vInsertBefore );
                } 
                else 
                {
                        vTarget.add ( vSource );
                };                        
        };
        
        // deselect
        if ( vTarget.getManager )
        {
                vTarget.getManager().deselectAll();
        };

        // sort
        if ( vTarget.getSortAfterDrop() && vTarget.sortItemsByString ) 
        {
                vTarget.sortItemsByString();
        };
        
        // update remote
        if ( vTarget.getUpdateRemoteOnChange() )
        {
                vTarget.updateRemote();
        };
        
        // if source is a child widget, get parent
        if ( vSource.getChildren().length == 0 )
        {
                vSource = vSource.getParent();
        };
        
        if ( vSource !== vTarget )
        {
                // sort
                if ( vSource.getSortAfterDrop() && vSource.sortItemsByString ) 
                {
                        vSource.sortItemsByString();
                };
                
                // update remote
                if ( vSource.getUpdateRemoteOnChange() )
                {
                        vSource.updateRemote();
                };
        };
        QxWidget.flushGlobalQueues();
};


/**
 * default drag move behaviour: display a marker line on top of the
 * element hovered over. for more sophisticated behaviour, override
 * this method
 * todo: the marker should adapt itself to the orientation of the
 * layout 
 */
QxWidget.prototype.handleDragMove = function (e)
{       
        var vTarget                     = e.getCurrentTarget(),
                vChildren               = vTarget.getChildren(),
                vEventTop               = e.getPageY()
                vInsertBefore   = vTarget._insertBefore,
                vMarker                 = vTarget.getDropMarker();

        // no marker if target gets sorted after the drop
        if ( vTarget.getSortAfterDrop() ) return;
                
        for ( var i=0; i < vChildren.length; i++ ) 
        {
                var vChild      = vChildren[i],
                        vElem   = vChild.getElement(),
                        vTop    = QxDom.getComputedPageBoxTop( vElem ),
                        vLeft   = QxDom.getComputedPageBoxLeft( vElem ),
                        vWidth  = QxDom.getComputedBoxWidth( vElem );
                
                // is dragged object between two elements
                if ( ( vTop - vEventTop ).betweenRange(-4, 4) ) 
                {
                        // save next element
                        vTarget._insertBefore = vChild; 
                        
                        // show marker
                        if ( vMarker )
                        {
                                vMarker.set({visibility:true, zIndex: 
vChild.getZIndex() +1,
                                                         top: vTop, left: 
vLeft, width: vWidth } );
                                QxWidget.flushGlobalQueues();
                        };
                };
        };
};

/**
 * handles the end of the drag event
 */
QxWidget.prototype.handleDragEnd = function (e)
{
        var vTarget                     = e.getTarget(),
                vMarker                 = vTarget.getDropMarker();
                
        if ( vMarker )
        {
                vMarker.setVisibility(false);
        };
        
        vTarget._insertBefore = null;
        vTarget._isBeingDragged = false;
};


//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------


/**
 * update local data from server and dispatch event if set
 **/
QxTarget.prototype.updateLocal = function ( callbackFunc )
{

        var self        = this,
                name    = this.getName(),
                manager = this.getDataManager();

        manager.transportReceive(
        {
                url: self._prepareUrl("receive"),
                params: null,
                handler: function ( data )
                {
                        manager._preventRemoteUpdate = true;
                        
                        // update properties
                        var property, setter;
                        for ( property in data)
                        {
                                setter = 'set' + 
property.substr(0,1).toUpperCase() + property.substr(1);
                                if ( setter=="setOptions" || setter=="setData" 
) continue;
                                if ( typeof self[setter]=="function" ){
                                        self[setter]( data[property] );
                                };
                        };                      
                        
                        // update options
                        if ( typeof data.options != "undefined" && typeof 
self.setOptions == "function" )
                        {
                                self.setOptions ( data.options );
                        };

                        // update data
                        if ( typeof data.data != "undefined" )
                        {
                                self.setLocalData ( data.data );
                        };
                                                
                        manager._preventRemoteUpdate = false;
                        
                        QxWidget.flushGlobalQueues();

                        if ( self.getDispatchOnDataReceived() )
                        {
                                manager.dispatch ( 
self.getDispatchOnDataReceived(), data );
                        };
                        
                        // callback
                        if ( typeof callbackFunc == "function" )
                        {
                                callbackFunc( data );
                        };                      
                }
        });
};

/**
 * update options only
 */
QxTarget.prototype.updateOptions = function ( callbackFunc )
{
        var self        = this,
                name    = this.getName(),
                manager = this.getDataManager() || new QxDataManager;

        manager.transportReceive(
        {
                url: self._prepareUrl("receive"),
                params: null,
                handler: function ( data )
                {
                        manager._preventRemoteUpdate = true;
                        
                        // update options
                        if ( typeof data.options != "undefined" && typeof 
self.setOptions == "function" )
                        {
                                self.setOptions ( data.options );
                        };
                
                        manager._preventRemoteUpdate = false;
                        QxWidget.flushGlobalQueues();
                        
                        // callback
                        if ( typeof callbackFunc == "function" )
                        {
                                callbackFunc( data );
                        };
                }
        });
};

/**
 * update server with local data and dispatch event if set
 **/
QxTarget.prototype.updateRemote = function ( callbackFunc )
{       
        var self        = this,
                name    = this.getName(),
                manager = this.getDataManager() || new QxDataManager,
                data    = {};
                
        if ( manager._preventRemoteUpdate ) return;
        
        data.data = this.getLocalData();
        
        if ( typeof this.getOptions == "function" ) 
        {
                data.options = this.getOptions();
        };
                
        manager.transportSend (
        {
                url: self._prepareUrl("send"),
                params: data,
                handler: function ( data )
                {
                        // event dispatcher
                        if ( self.getDispatchOnDataSent() )
                        {
                                manager.dispatch ( 
self.getDispatchOnDataSent(), data );
                        }
                        
                        // callback
                        if ( typeof callbackFunc == "function" )
                        {
                                callbackFunc( data );
                        };              
                }
        });
};


/**
 * gets element data
 **/
QxTarget.prototype.getLocalData  = function() {
        
        return this[this.getGetter()]();
};

/**
 * sets element data
 **/
QxTarget.prototype.setLocalData = function( value ) {
        this._previousData = this.getLocalData();
        this[this.getSetter()]( value || '' );
};

/** 
 * resets to previous data
 */
QxTarget.prototype.reset = function ()
{
        this.setLocalData( this._previousData );
}

/** 
 * empties data
 */
QxTarget.prototype.empty = function ()
{
        this.setLocalData( '' );
};



//--------------------------------------------------------------------------
// QxList
//--------------------------------------------------------------------------


/**
 * QxList-specific interface for getting options
 * @return array of all list item values
 **/
QxList.prototype.getOptions = function ()
{
        var options=[], i, 
                vItems=this.getChildren(), 
                vLength=vItems.length;
        
        for ( i=0; i<vLength; i++) {
                var value = vItems[i].getValue();
                if ( value !== false && value !== null ) {
                        options.push(value);
                }
        };
        return options;
};

/**
 * QxList-specific interface for setting options
 * @param array data array of list items
 **/
QxList.prototype.setOptions = function ( options )
{
        if ( typeof options == "string ")
        {
                options = options.split(",");
        }
        
        this.removeAll();
        
        for ( var i=0; i < options.length; i++ ) 
        {
                if ( typeof options[i] == "object" ) {
                        this.add ( new QxListItem ( options[i].label, null, 
options[i].value ) );
                } else {
                        this.add ( new QxListItem ( options[i], null, 
options[i] ) );
                };
        };
};

/**
 * QxList-specific interface for getting data, i.e. selection
 * @return array of selected values
 **/
QxList.prototype.getLocalData = function ()
{
        var i, values=[],
                vItems=this.getSelectedItems(), 
                vLength=vItems.length;
                
        for ( i=0; i<vLength; i++) {
                values.push( vItems[i].getValue() );
        };
        
        return values;
};


/**
 * set list data, i.e. selecte list items which have
 * values present in the data
 **/    
QxList.prototype.setLocalData = function ( value )
{
        var i, items=[],
                data =( typeof value == "string" ) ? value.split(",") : value;
        
        if ( data.length == 1 )
        {
                this.getManager().setSelectedItem ( this.findValueExact ( value 
) );
        }
        else
        {
                for ( i=0; i<data.length; i++)
                {
                        items.push ( this.findValueExact( data[i] ) );
                };
                this.getManager().setSelectedItems ( items );
        };
        
        QxWidget.flushGlobalQueues();
};


//--------------------------------------------------------------------------
// QxComboBox
//
// Note that this extension changes the behaviour of setValue() and getValue()
// Use "element.getLocalData()" to retrieve the real content of the combobox
//--------------------------------------------------------------------------

// autocomplete behaviour property
QxComboBox.addProperty({ name : "autocomplete", type : QxConst.TYPEOF_BOOLEAN, 
defaultValue : false });

// where to get autocomplete data from
QxComboBox.addProperty({ name : "autocompleteUrl", type : 
QxConst.TYPEOF_STRING, defaultValue : '' });

// a separator marks the beginning of a new autocomplete section
QxComboBox.addProperty({ name : "separator", type : QxConst.TYPEOF_STRING, 
defaultValue : '' });

/**
 * whether to send data when local data changes
 */
QxComboBox.prototype._modifyUpdateRemoteOnChange = function(propValue, 
propOldValue, propData)
{
        if ( propValue )
        {
                this.getField().addEventListener ( "changeValue", 
this.updateRemote ,this ); 
        } 
        else if ( propOldValue )
        {
                this.getField().removeEventListener ( "changeValue", 
this.updateRemote ,this ); 
        };
        return true;
};

 /**
  * turns autocomplet behaviour on and off 
 */
QxComboBox.prototype._modifyAutocomplete = function(propValue, propOldValue, 
propData)
{
        if ( propValue )
        {
                this.setEditable(true);
        } 
        else if ( propOldValue )
        {
                //
        };
        return true;
};

/**
 * overrides default _oninput event handler
 * handles an textfield input by repopulating the combobox list and proposing 
 * a match
 * receiveUrl: /keywords/autocomplete with get/post param data={word to be 
matched}
 * response: { options: (array of suggestions), data:"(text to be 
autocompleted)" }
 */
QxComboBox.prototype._oninput = function ( e )
{
        // flag to prevent matching
        if ( this._preventMatch ){
                this._preventMatch = false;
                return true;
        };
        
        var self                        = this,
                vUrl                    = this._prepareUrl("autocomplete"),
                vManager                = this.getDataManager(),
                vValue                  = e.getData(),
                vSeparator              = this.getSeparator(),
                vStartMatchAt   = vSeparator ? ( vValue.lastIndexOf ( 
vSeparator ) + 1 ) : 0,
                vBeforeMatch    = vStartMatchAt ? vValue.substr ( 0, 
vStartMatchAt ) : "",
                vMatch                  = vValue.substr( vStartMatchAt );

        this._fromInput = true;
        this.setValue( vValue );
        this._fromInput = false;
        
        if ( ! dojo.string.trim( vMatch ).length ) {
                this._popup.hide();
                this._list.removeAll();
                return true;
        };

        // execute with a timeout to prevent requests during rapid typing
        window.setTimeout ( function() 
        {
                if ( vValue != self._field.getComputedValue() ) {
                        // user has typed ahead in the meantime, abort
                        return;
                }
                
                vManager.transportReceive({
                        url: vUrl,
                        params: { data: vMatch },
                        handler: function ( data ){
                                
                                if ( vValue != self._field.getComputedValue() ) 
{
                                        // user has typed ahead in the 
meantime, abort
                                        return;
                                };
                                
                                // update options
                                if ( typeof data.options != "undefined" && 
typeof self.setOptions == "function" )
                                {
                                        self.setOptions ( data.options );
                                        self._openPopup();
                                };
                                
                                // update data
                                if ( typeof data.data != "undefined" )
                                {
                                        self.setValue ( data.data );
                                };
                                QxWidget.flushGlobalQueues();
                        }
                });
        },200 );
};


/**
 * overrides QxCombobox selected modifier
 * necessary because default behaviour does not support partial replacement of 
input
 * @author Sebastian Werner
 * @author Andreas Ecker
 * modfied by CB;
 * - popup doesn't open on "enter", but on typing and the arrow keys
 */
QxComboBox.prototype._modifySelected = function(propValue, propOldValue, 
propData)
{
  this._fromSelected = true;

  // only do this if we called setSelected seperatly
  // and not from the property "value".
  if ( ! this._fromValue ) {
        this.setValue( propValue ? propValue.getLabel() : QxConst.CORE_EMPTY);  
  };
                
  // be sure that the item is in view
  if ( this.getPopup().isSeeable() && propValue ) {
    propValue.scrollIntoView();
  };
        
  // reset manager cache
  this._manager.setLeadItem(propValue);
  this._manager.setAnchorItem(propValue);

  // sync to manager
  this._manager.setSelectedItem(propValue);
  
  // return focus to textfield
  this._field.setFocused ( true );
  
  // prevent matching, since this is a value from the popup list
   this._preventMatch = true;

  // reset hint
  delete this._fromSelected;

  return true;
};

/**
 * overrides QxCombobox value modifier
 * @author Sebastian Werner
 * @author Andreas Ecker
 * modfied by CB;
 */
QxComboBox.prototype._modifyValue = function(propValue, propOldValue, propData)
{
  // no further action if this is called from an input event
  if ( this._fromInput ){
        this._originalValue = propValue;
        return true;
  };
  
  this._fromValue = true;
  
  // only do this if we called setValue seperatly
  // and not from the property "selected".
  if ( ! this._fromSelected )
  {
    // inform selected property
    var vSelItem = this._list.findStringExact(propValue);

    // ignore disabled items
    if (vSelItem != null && !vSelItem.getEnabled()) {
      vSelItem = null;
    };

    this.setSelected(vSelItem);
   
    // be sure that the manager get informed
    // if 'selected' was already 'null'
    if (vSelItem == null) {
      this._manager.deselectAll();
    };
  }
  else
  {
        this._field.setValue ( propValue );
  }
        
        // CB: added autocomplete behaviour
        if ( this.getAutocomplete() )
        {
                var vSeparator          = this.getSeparator(),
                        vOldValue               = this._originalValue || 
propOldValue || "",
                        vStartMatchAt   = vSeparator ? ( vOldValue.lastIndexOf 
( vSeparator ) + 1 ) : 0,
                        vBeforeMatch    = vStartMatchAt ? vOldValue.substr ( 0, 
vStartMatchAt ) : "",
                        vMatch                  = vOldValue.substr( 
vStartMatchAt ),
                        vNewValue               = vBeforeMatch + propValue,
                        vOldLength              = vOldValue.length,
                        vNewLength              = vNewValue.length;
        
                // set value without updating the server
                this._preventRemoteUpdate = true;
                this._field.getElement().value = vNewValue;
                this._preventRemoteUpdate = false;
                
                // set textfield selection
                this._field.setFocused ( true );
                this._field.setSelectionStart ( vOldLength );
                this._field.setSelectionLength ( vNewLength - vOldLength );     
                this._preventMatch = true;
        };
        
  // reset hint
  delete this._fromValue;

  return true;
};

/**
 * overrides onkeydown event handler 
 * @author Sebastian Werner
 * @author Andreas Ecker
 * modified by CB:
 * - disabled QxList key events so that "home" and "line end" keys do not 
disturb input
 */
QxComboBox.prototype._onkeydown = function(e)
{
  var vManager = this._manager;
  var vKeyCode = e.getKeyCode();
  var vKeys = QxKeyEvent.keys;
  var vVisible = this._popup.isSeeable();

  switch(vKeyCode)
  {
    // Handle <ENTER>
    case vKeys.enter:
      if (vVisible)
      {
        this._closePopup();     
        this.setSelected(this._manager.getSelectedItem());
      };
      return;

    // Handle <ESC>
    case vKeys.esc:
      if (vVisible)
      {
        vManager.setLeadItem(this._oldSelected);
        vManager.setAnchorItem(this._oldSelected);

        vManager.setSelectedItem(this._oldSelected);

        this.setValue(this._oldSelected ? this._oldSelected.getLabel() : 
QxConst.CORE_EMPTY);
        this._closePopup();
      };

      return;

    // Handle <PAGEUP>
    case vKeys.pageup:
      if (!vVisible)
      {
        var vPrevious;
        var vTemp = this.getSelected();

        if (vTemp)
        {
          var vInterval = this.getPagingInterval();

          do {
            vPrevious = vTemp;
          } while(--vInterval && (vTemp = vManager.getPrevious(vPrevious)));
        }
        else
        {
          vPrevious = vManager.getLast();
        };

        this.setSelected(vPrevious);
        return;
      };

      break;

    // Handle <PAGEDOWN>
    case vKeys.pagedown:
      if (!vVisible)
      {
        var vNext;
        var vTemp = this.getSelected();

        if (vTemp)
        {
          var vInterval = this.getPagingInterval();

          do {
            vNext = vTemp;
          } while(--vInterval && (vTemp = vManager.getNext(vNext)));
        }
        else
        {
          vNext = vManager.getFirst();
        };

        this.setSelected(vNext);
        return;
      };

      break;
  };

  // key handling
  
  switch(vKeyCode)
  {
    case vKeys.pageup:
    case vKeys.pagedown:
      if (!this._popup.isCreated()) {
        return;
      };
      // no break here

    case vKeys.up:
    case vKeys.down:
      this._list._onkeydown(e);
      // no break here
      
    default:
      this.setSelected(this._manager.getSelectedItem());
      break;
  };
};


/**
 * get combobox options, disable so that options don't get sent back to the 
server
 * during update
 **/
QxComboBox.prototype.getOptions = null;

/**
 * set combobox options
 **/
QxComboBox.prototype.setOptions = function ( options )
{
        return this.getList().setOptions ( options );
};

/**
 * get combobox data
 * use this function instead of getValue()
 **/
QxComboBox.prototype.getLocalData = function ()
{
        if ( this.getEditable() )
        {
                return this._field.getElement().value;
        }
        else
        {
                var item = this.getList().findStringExact( this.getValue() );
                return item ? item.getValue() : "";
        };
};

/**
 * set combobox data; needs to keep the "value" seperately from the "label" 
displayed
 **/
QxComboBox.prototype.setLocalData = function ( value )
{
        if ( this.getEditable() )
        {
                this._field.getElement().value = ( value || "" );
        }
        else
        {
                var item = this.getList().findValueExact( value );
                this.getList().getManager().setSelectedItem ( item );
                this._field.getElement().value = ( item ? item.getLabel() : 
value );
        };
        return true;
};

//--------------------------------------------------------------------------
// QxRadioManager
//--------------------------------------------------------------------------

/**
 * get radio manager value
 **/
QxRadioManager.prototype.getLocalData = function ()
{
        return this.getSelected().getValue();   
};

/**
 * set radio manager value
 **/
QxRadioManager.prototype.setLocalData = function ( value )
{
        var i, vItems = this._items, vLength=vItems.length;
        
        for ( i=0 ; i<vLength; i++) {
                
                if ( vItems[i].getValue() == value ) {
                        this.setSelected( vItems[i] );
                        break;
                };
        };
};

//--------------------------------------------------------------------------
// QxCheckBox
//--------------------------------------------------------------------------

/**
 * QxCheckBox-specific interface for getting content
 **/    
QxCheckBox.prototype.getLocalData = function ( value )
{
        this.getChecked ();
};

/**
 * QxCheckBox-specific interface for setting content
 **/
QxCheckBox.prototype.setLocalData = function(value)
{
        this.setChecked ( value );
};

//--------------------------------------------------------------------------
// QxLabel
//--------------------------------------------------------------------------

/**
 * QxLabel-specific interface for setting content
 **/
QxLabel.prototype.setLocalData = function( value ) 
{
        this.setHtml ( "<span>" + (value||'') + "</span>" );
};

//--------------------------------------------------------------------------
// QxListView
//--------------------------------------------------------------------------

// multiselection
QxListView.addProperty({ name : "multiSelection", type : 
QxConst.TYPEOF_BOOLEAN, defaultValue : false });

/**
 * enable or disable multiselection
 */
QxListView.prototype._modifyMultiSelection = function(propValue, propOldValue, 
propData)
{
        this.getPane().getManager().setMultiSelection( propValue ); 
        return true;
};


/**
 * listen for change event event and dispatch passed event name
 */
QxListView.prototype._modifyDispatchOnChange = function(propValue, 
propOldValue, propData)
{
        if ( propValue )
        {
                this.getPane().getManager().addEventListener ( 
"changeSelection", function() {
                        this.getPane().getManager().setDragSelection(false);
                        this.getDataManager().dispatch ( propValue, 
this.getLocalData() );
                },this); 
        }; 
        return true;
};

// row id lookup index
QxListView.prototype._rowById = [];

/**
 * setting QxListView options, i.e, setting the rows
 **/
QxListView.prototype.setOptions = function ( data )
{
         // empty grid
         this._rowById = [];
         this._data.splice(0,this._data.length);
         this.update();
                        
        // build new grid
        if ( ! data.length ) return;
        
        for ( var i=0; i < data.length; i++ ){
            
            // set default row id if missing
            if ( typeof data[i].id == "undefined" || typeof data[i].id.text == 
"undefined" ){
                data[i].id = {text:i};
            };
            
            // set row data
            this._data.push( data[i] );
            this._rowById[ data[i].id.text ] = i;
        };
                        
    this.updateSort();
        this.update();
        QxWidget.flushGlobalQueues();
};
 
// don't provide a getOptions methods, otherwise all listview rows
// will be sent back to the server

/**
 * getting QxListView data, i.e. the selected row ids which
 * need to be in the grid row data as { id: { text: ID }, ...}
 * @return array 
 **/
QxListView.prototype.getLocalData = function ()
{
        var i, vRows = [], vRowNo,
                vItems = this.getPane().getSelectedItems();
                        
        for ( i=0; i < vItems.length; i++ ){
                vRows.push ( vItems[i].id ? ( vItems[i].id.text ) : i );
        };
        return vRows;
};

/**
 * setting QxListView data, i.e. the select the rows according to the
 * array with ids list
 * todo: there is a bug here! selection is not correct
 * @return array 
 **/
QxListView.prototype.setLocalData = function ( ids )
{
        var i, j, id, vFirst, rowNo,
                vManager        = this.getPane().getManager(),
                vData           = this.getData();
                
        vManager.deselectAll();

        for (j=0; j <ids.length; j++ ){
                
                rowNo = this._rowById[ ids[j] ];
                vManager.setItemSelected( vData[rowNo], true );
                if ( ! vFirst )
                {
                        vFirst = vData[rowNo];
                };
        };

        if ( vFirst ) {
                this.getPane().scrollItemIntoViewY ( vFirst, 0 );
        };
};



//--------------------------------------------------------------------------
// QxDataManager
//--------------------------------------------------------------------------

/**
 * QxDataManager
 * 
 * provides a manager for enhanced form elements or other widgets 
 * which automagically get their data from the server and update the 
 * server when changed.
 * 
 * A major conceptual difference to HTML forms is that the information
 * about which data on the server is bound to the fields can, but does not 
 * need to be stored at the "form" level. You can also bind each element 
 * to a different datasource on the server. A "form" or dialogue can thus be 
 * populated from and update its value at many different sources. 
 * The data manager then serves as a reference point to connect the 
 * widgets which otherwise wouldn't know about each other. 
 * 
 * This concept also allows to integrate non-traditional, read-only form 
 * elements like listviews, trees, displayboxes, etc.
 * 
 * You can use whatever transport mechanism you like by overriding
 * the tranportSend and tranportReceive methods of this object. The default
 * method relies on a homegrown transport mechanism based on dojo.
 * 
 * Data binding
 * ------------
 * method                                               determines whether the 
data should be sent received or sent 
 *                                                              as "get" or 
"post" (default). string.
 * sendUrl                                              url where to fetch the 
data remotely. %foo is replaced by the 
 *                                                              value of 
element with name foo. string.
 * receiveUrl                                   url with which to update data 
remotely. %foo is replaced by the 
 *                                                              value of 
element with name foo. string.
 * 
 * Data events
 * -----------
 * dispatchOnDataSent                   event to dispatch when server has 
acknowledged the receipt of 
 *                                                              the data sent. 
string.
 * dispatchOnDataReceived               event to dispatch when server has sent 
data. string.
 * updateRemoteOnEvent                  event name which triggers remote 
update. string.
 * updateLocalOnEvent                   event name which triggers local update. 
string.
 *
 */

function QxDataManager ( )
{
        if ( ! window.zophe || ! window.dojo )
        {
                throw new Error ("QxDataManager: zophe/dojo is not present. " +
                                                 "You need to override the 
transportSend, transportReceive " +
                                                 "and dispach methods with your 
own implementation.");
        }
        
        QxTarget.call(this);
        
        // internal data
        this._dataWidgets       = [];
        this._hiddenFields      = {};
};

QxDataManager.extend(QxTarget, "QxDataManager");

//--------------------------------------------------------------------------
// Remove QxTarget Properties 
//--------------------------------------------------------------------------

QxDataManager.removeProperty({ name : "dataManager"});
QxDataManager.removeProperty({ name : "updateRemoteOnChange" });
QxDataManager.removeProperty({ name : "dispatchOnChange" });

//--------------------------------------------------------------------------
// Methods that need to be adapted to environment
//--------------------------------------------------------------------------

/**
 * hooks to insert your favorite transport mechanism. the kwArgs 
 * parameter should have the following keys defined:
 * - method
 * - url
 * - params hash of key-value pairs to be sent 
 * - handler function which is called after request has been completed
 *   in tranportReceive the data received is passed to the handler
 */
proto.transportSend = function ( kwArgs )
{
        // we don't need kwArgs.method since everything is sent via post
        zophe.io.call ( kwArgs.url, kwArgs.params, kwArgs.handler );
};

proto.transportReceive = function ( kwArgs )
{
        // we don't need kwArgs.method since everything is sent via post
        zophe.io.request ( kwArgs.url, kwArgs.params, kwArgs.handler );
};

/**
 * dispatch event method, override with your own solution
 * I want to dispatch events globally, but you might want to 
 * restrict the scope of the event to the data manager.
 */
proto.dispatch = function (name, value)
{
        var event = {}; event[name] = value; zophe.event.dispatch ( event );
        //window.application.createDispatchDataEvent ( name, value );
        //this.createDispatchDataEvent ( name, value );
};


//--------------------------------------------------------------------------
// Public methods
//--------------------------------------------------------------------------

/**
 * overrides QxTarget function
 * 
 */
proto.getDataManager = function()
{
        return this;
};

/**
 * adds a form widget to the form
 * @param object widget
 **/
proto.add = function( vWidget )
{
        var vName = vWidget.getName();
        this._dataWidgets.push (vWidget);
        vWidget.setDataManager( this );
};

/**
 * removes a form widget from the form
 * @param string vWdiget
 **/
proto.remove = function( vWidget )
{
        var vName = vWidget.getName();
        vWidget.setDataManager( null );
        for (var i=0; i<this._dataWidgets.length; i++)
        {
                if ( this._dataWidgets[i].getName()== vName )
                {
                        delete this._dataWidgets[ i ];
                };
        };
};

/**
 * gets all values from the bound field widgets 
 * and the hidden fields
 */
proto.getLocalData = function()
{
        var i, vName, data = {};
        
        // data widgets
        for ( i=0; i<this._dataWidgets.length; i++ ) 
        {
                vName = this._dataWidgets[i].getName();
                // only take first widget data if there are several with the 
same name
                if ( typeof data[vName] == "undefined" )
                { 
                        data[vName] = this._dataWidgets[i].getLocalData();
                };
        };
        
        // hidden fields
        for ( vName in this._hiddenFields )
        {
                data[vName] = this._hiddenFields[vName];
        };
        return data;
};

/**
 * populates all associated elements with data
 * overrides QxTarget.setLocalData
 * @param object hash of key-value pairs, keys must correspond to the form 
element names
 */
proto.setLocalData = function ( data )
{
        var i, vName, vFields, vProp, vSetter;
        
        for ( var vName in data ) 
        {
                vFields = this.getChildElements( vName );
                vData   = data[vName];
                
                if ( vFields.length > 0 ) 
                {
                        // populate all fields with the same name
                        // and set their properties
                        for ( i=0; i<vFields.length; i++)
                        {
                                for ( vProp in vData)
                                {
                                        vSetter = 'set' + 
vProp.substr(0,1).toUpperCase() + vProp.substr(1);
                                        if ( vSetter=="setOptions" || 
vSetter=="setData" ) continue;
                                        if ( typeof 
vFields[i][vSetter]=="function" ){
                                                vFields[i][vSetter]( 
vData[vProp] );
                                        };
                                };                      
                                
                                // update options
                                if ( typeof vData.options != "undefined" && 
typeof vFields[i].setOptions == "function" )
                                {
                                        vFields[i].setOptions ( vData.options );
                                };
        
                                // update data
                                if ( typeof vData.data != "undefined" )
                                {
                                        vFields[i].setLocalData ( vData.data );
                                };                              
                        };
                }
                else 
                {
                        // save data in hidden field
                        this.setHiddenField (vName, vData );
                        
                };
        };
        return true;
};

/**
 * get the first form element with th given name
 */
proto.getChildElement = function ( vName )
{ 
        for ( var i=0; i<this._dataWidgets.length; i++ ) 
        {
                if ( vName == this._dataWidgets[i].getName() )
                { 
                        return this._dataWidgets[i];
                };
        };
        return false;
};

/**
 * get all elements with a given name
 */
proto.getChildElements = function ( vName )
{ 
        var vList = [];
        for ( var i=0; i<this._dataWidgets.length; i++ ) 
        {
                if ( vName == this._dataWidgets[i].getName() )
                { 
                        vList.push (this._dataWidgets[i]);
                };
        };
        return vList;
};

/**
 * get all child elements
 */
proto.getChildren = function()
{
        return this._dataWidgets;
}
/**
 * gets the data from a form element or hidden field
 */
proto.getFieldData = function ( vName )
{
        var child = this.getChildElement( vName );
        if ( typeof child == "object" )
        {
                return child.getLocalData();
        }
        else
        {
                return this.getHiddenField ( vName );
        };
};

/**
 * sets the data from a form element or hidden field
 */
proto.setFieldData = function ( vName, vData )
{
        var child = this.getChildElement( vName );
        if ( typeof child == "object" )
        {
                child.setLocalData ( vData );
        }
        else
        {
                this.setHiddenField ( vName, vData );
        };
        return true;
};

/**
 * clears a form
 */
proto.empty = function( )
{
        for ( var i=0; i< this._dataWidgets.length; i++ ) {
                this._dataWidgets[i].empty();
        };
};

/**
 * clears a form
 */
proto.reset = function( )
{
        for ( var i=0; i< this._dataWidgets.length; i++ ) {
                this._dataWidgets[i].reset();
        };
};

/**
 * set hidden field
 * @param string field name
 * @param mixed value
 */
proto.setHiddenField = function ( vName, vValue )
{
        this._hiddenFields[vName] = vValue;
        return true;
}

/**
 * get hidden field
 * @param string field name
 */
proto.getHiddenField = function ( vName )
{
        return this._hiddenFields[vName];
}

/**
 * flag to prevent remote update when updating locally
 */
proto._preventRemoteUpdate = false;

/**
 * sets prevent-remote-update flag
 */
proto.setPreventRemoteUpdate = function ( vValue )
{
        this._preventRemoteUpdate = vValue;
};

/**
 * dispose form
 */
proto.dispose = function()
{
        if (this.getDisposed()) {
           return true;
        };
        this._dataWidgets = null;
        return QxTarget.prototype.dispose.call(this);
};
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys -- and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
qooxdoo-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/qooxdoo-devel

Reply via email to