Document Information

Last modified:
2008/04/03 18:23 by c.boulanger

Asynchronous (Non-Blocking) User Interaction

JavaScript’s commands to interact with the user (alert, confirm, prompt) are extremely limited and they have a very nasty side-effect: they block the UI and all javascript processes, such as redrawing the layout, rpc requests, etc.

Here is a mixin for qx.application.Gui that provides the following methods:

  • inform : inform the user
  • alert : warn the user
  • confirm: let the user confirm something
  • offer : give the user a set of choices
  • prompt : let the user enter some text
  • presentForm : presents a form with several fields
  • upload : lets the user choose a file to upload

All functions are non-blocking and therefore require a callback function that is called when the user has reacted to the information presented.

/**
 * Mixin for application providing asynchronous
 * equivalents for prompt, alert, confirm
 *
 * Author: Christian Boulanger
 * This is public domain, you can do whatever you want with it.
 * Please share your improvements of the code and report them to  info at bibliograph dot org
 */
qx.Mixin.define("custom.MUserInteraction",
{
  members :
  {
    /**
     * Displays an information message
     *
     * @type member
     * @param msg {String} Message 
     * @param callback {Function} Callback function
     * @param context {Object} "this" object in callback function
     * @return {void} 
     */
    inform : function(msg, callback, context)
    {
      // "OK" button
      var b = new qx.ui.form.Button(this.tr("OK"), "icon/16/actions/dialog-ok.png");
      var controls = [ b ];
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/status/dialog-information.png", "icon/32/status/dialog-information.png", controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
 
        if (typeof (callback) == "function") {
          callback.call(context);
        }
      });
    },
 
 
    /**
     * Displays an alert / warning
     *
     * @type member
     * @param msg {String} Message 
     * @param callback {Function} Callback function
     * @param context {Object} "this" object in callback function
     * @return {void} 
     */
    alert : function(msg, callback, context)
    {
      // "OK" button
      var b = new qx.ui.form.Button(this.tr("OK"), "icon/16/actions/dialog-ok.png");
      var controls = [ b ];
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/status/dialog-warning.png", "icon/32/status/dialog-warning.png", controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
 
        if (typeof (callback) == "function") {
          callback.call(context);
        }
      });
    },
 
 
    /**
     * Asks user to confirm something. The callback function receives a true or false value.
     *
     * @type member
     * @param msg {String} Message 
     * @param yesMsg {String} Label for the "Yes" or "OK" button (Default: "OK")
     * @param noMsg {String} Label for the "No" or "Cancel" button (Default: "Cancel")
     * @param callback {Function} Callback function
     * @param context {Object} "this" object in callback function
     * @return {void} 
     */
    confirm : function(msg, yesMsg, noMsg, callback, context)
    {
      // make sure callback is a function
      this._checkCallback(callback);
 
      // cancel button
      var c = new qx.ui.form.Button(noMsg || this.tr("Cancel"), "icon/16/actions/dialog-cancel.png");
 
      // "OK" button
      var b = new qx.ui.form.Button(yesMsg || this.tr("OK"), "icon/16/actions/dialog-ok.png");
 
      // button panel
      var p = new qx.ui.layout.HorizontalBoxLayout;
      p.setSpacing(10);
      p.setHorizontalChildrenAlign("center");
      p.add(c, b);
 
      // controls for window
      var controls = [ p ];
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/actions/help-about.png", "icon/32/status/help-about.png", controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, true);
      });
 
      // add event listener for cancel Button
      c.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, false);
      });
    },
 
 
    /**
     * Offers a set of choices, one button for each. The callback
     * function receives a integer (1 ... n) according to which
     * button the user clicks on 
     *
     * @type member
     * @param msg {String} Message 
     * @param choices {Array} Array of labels for buttons
     * @param callback {Function} Callback function
     * @param context {Object} "this" object in callback function
     * @return {void} 
     */
    offer : function(msg, choices, callback, context)
    {
      // make sure callback is a function
      this._checkCallback(callback);
 
      // check choices
      if (!choices instanceof Array) {
        this.error("Second argument must be an array.");
      }
 
      // button panel
      var p = new qx.ui.layout.HorizontalBoxLayout;
      p.setSpacing(10);
      p.setHorizontalChildrenAlign("center");
 
      // controls for window
      var controls = [ p ];
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/actions/help-about.png", "icon/32/status/help-about.png", controls);
 
      // make a button for each choice and add the required event listener
      var i = 1;
 
      choices.forEach(function(choice)
      {
        var b = new qx.ui.form.Button(choice);
        b.setHeight(24);
        eval('b.addEventListener("execute",function(){' + 'w.close();w.dispose();' + 'callback.call(context,' + (i++) + ');' + '});');
        p.add(b);
      });
 
      // cancel button
      var c = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png");
 
      c.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, false);
      });
 
      p.add(c);
    },
 
 
    /**
     * Prompts the user to enter a text.
     *
     * @type member
     * @param msg {String} Message 
     * @param defaultAnswer {String} Default answer displayed in the text field
     * @param callback {Function} Callback function
     * @param context {Object} "this" object in callback function
     * @param lines {Int} number of lines. If larger than 1, a TextArea widget is used 
     * @return {void} 
     */
    prompt : function(msg, defaultAnswer, callback, context, lines)
    {
      // make sure callback is a function
      this._checkCallback(callback);
 
      // cancel button
      var c = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png");
 
      // "OK" button
      var b = new qx.ui.form.Button(this.tr("OK"), "icon/16/actions/dialog-ok.png");
 
      // text field
      if (lines && lines > 1)
      {
        var t = new qx.ui.form.TextArea(defaultAnswer);
        t.setHeight(lines * 16);
      }
      else
      {
        var t = new qx.ui.form.TextField(defaultAnswer);
      }
 
      t.setWidth("100%");
 
      t.addEventListener("appear", function() {
        t.selectAll();
      });
 
      // button panel
      var p = new qx.ui.layout.HorizontalBoxLayout;
      p.setSpacing(10);
      p.setHorizontalChildrenAlign("center");
      p.add(c, b);
 
      // controls for window
      var controls = [ t, p ];
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/actions/help-about.png", "icon/32/status/help-about.png", controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        var v = t.getValue();
        w.close();
        w.dispose();
        callback.call(context, v);
      });
 
      // add event listener for cancel Button
      c.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, false);
      });
    },
 
    /**
     * Presents a form with multiple fields. 
     *
     * @type member
     * @param msg {String} Message 
     * @param formData {Map} Map of form field information: 
     * <pre>
     * { 'username' : { 'label':"User Name", 'value':"", 'lines':1 } }
     * </pre>
     * @param callback {Function} Callback function, argument of function will be formData array with updated value properties
     * @param context {Object} "this" object in callback function
     * @return {void} 
     */
    presentForm : function (msg, formData, callback, context)
    {
      // check form data
      if ( typeof formData != "object" )
      {
        this.error("Form data must be a map.");
      }
			
      // controls
      var controls = [];
			
      // make sure callback is a function
      this._checkCallback(callback);
 
      // cancel button
      var c = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png");
 
      // "OK" button
      var b = new qx.ui.form.Button(this.tr("OK"), "icon/16/actions/dialog-ok.png");
			
      // loop through form data array
      for ( key in formData )
      {
	      
           var fieldData = formData[key];
				
	    // label
	    var l = new qx.ui.basic.Label(fieldData.label);
            l.setWidth("1*");
				
             // text field
	      if ( fieldData.lines && fieldData.lines > 1)
	      {
	        var t = new qx.ui.form.TextArea(fieldData.value || "");
	        t.setHeight(lines * 16);
	      }
	      else
	      {
	        var t = new qx.ui.form.TextField(fieldData.value || "");
					t.setHeight(24);
	      }
	      t.setWidth("3*");
              t.setLiveUpdate(true);
              eval('t.addEventListener("changeValue", function(event){'+	
                 'formData.' + key + '.value=event.getData();' +
               '});');
				
               // panel
               var h = new qx.ui.layout.HorizontalBoxLayout;
               h.setWidth("100%");
               h.setHeight("auto");
               h.setSpacing(5);
               h.add(l,t);
               controls.push(h);
				
      }
 
      // button panel
      var p = new qx.ui.layout.HorizontalBoxLayout;
      p.setSpacing(10);
      p.setHorizontalChildrenAlign("center");
      p.add(c, b);
			controls.push(p);
 
      // window
      var w = this._createWindow(this.tr("Information"), msg, "icon/16/actions/help-about.png", "icon/32/status/help-about.png", controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, formData);
      });
 
      // add event listener for cancel Button
      c.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, false);
      });
    },
 
    /**
     * upload window. user can select a file on the local filesystem
     * @param {String} msg
     * @param {String|null} url or null if the file is not to be uploaded 
     * @param {Function} callback, will be called with the path of the local file as argument
     * @param {Object} context
     */
    upload : function (msg, url, callback, context)
    {
      // check that we have the upload widget available
      if (! window.uploadwidget )
      {
        return this.alert("Upload Widget is not available.");
      }
      
      // make sure callback is a function
      this._checkCallback(callback);
 
      // cancel button
      var c = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png");
 
      // "OK" button
      var b = new qx.ui.form.Button(this.tr("OK"), "icon/16/actions/dialog-ok.png");
      
      // form
      var cv = new qx.ui.layout.CanvasLayout;
      cv.addToDocument();
      cv.set({width:300,height:50});
      
      var form = new uploadwidget.UploadForm('uploadFrm',url );
      form.setParameter('rm','upload');
      form.set({top:0,left:0,right:0,bottom:0,width:null});
      
      // file upload field
      var file = new uploadwidget.UploadField('uploadFile',"Choose File",null);
      file.set({top:0,left:0,right:0,bottom:0,width:null});
      form.add(file);
      cv.add(form);
 
      // event listeners
      form.addEventListener('sending',function(e) {
        this.debug('sending');
      });
      
      form.addEventListener('completed',function(e) {
        var response = this.getIframeHtmlContent();
        this.debug(response);
        var path = file.getValue();
        w.close();
        w.dispose();
        callback.call(context, path );
      });
 
      // button panel
      var p = new qx.ui.layout.HorizontalBoxLayout;
      p.setSpacing(10);
      p.setHorizontalChildrenAlign("center");
      p.add(c, b);
      controls = [cv,p];
 
      // window
      var w = this._createWindow(this.tr("Select a file"), msg, 'icon/16/actions/document-save.png', 'icon/32/actions/document-save.png', controls);
 
      // add event listener for OK Button
      b.addEventListener("execute", function()
      {
        // send form if an url has been passed        
        this.setLabel("Uploading...");
        this.setEnabled(false);       
        if (url) 
        {
          form.send();  
        }
        else
        {
          var path = file.getValue();
          w.close();
          w.dispose();
          callback.call(context, path );        
        }
      });
 
      // add event listener for cancel Button
      c.addEventListener("execute", function()
      {
        w.close();
        w.dispose();
        callback.call(context, false);
      }); 
          
    },
    
 
 
    /**
     * checks if callback argument is a function
     *
     * @type member
     * @param callback {Function} TODOC
     * @return {Boolean} 
     */
    _checkCallback : function(callback)
    {
      if (typeof (callback) != "function") {
        this.error("Callback is not a function!");
      }
    },
 
 
    /**
     * Creates the popup window
     *
     * @type member
     * @param caption {String} Window caption
     * @param msg {String} Message
     * @param smallIcon {String} Resource string for icon in the window title bar
     * @param bigIcon {String} Resource string for icon in the main pane
     * @param controls {Array} Array of widgets that will be placed below each other in the main pane
     * @return {qx.ui.window.Window} 
     */
    _createWindow : function(caption, msg, smallIcon, bigIcon, controls)
    {
      // window
      var w = new qx.ui.window.Window(caption, smallIcon);
      w.setMinWidth(300);
      w.setMinHeight(100);
      w.setShowMaximize(false);
      w.setShowMinimize(false);
      w.setShowClose(false);
      w.setModal(true);
      w.setMoveable(false);
      w.setResizable(false);
 
      w.addEventListener("appear", function() {
        this.centerToBrowser();
      }, w);
 
      // layout
      var l = new qx.ui.layout.VerticalBoxLayout;
      l.setLeft(10);
      l.setRight(10);
      l.setBottom(10);
      l.setTop(10);
      l.setVerticalChildrenAlign("middle");
      l.setHorizontalChildrenAlign("center");
      l.setSpacing(10);
      l.setPadding(10);
      w.add(l);
 
      // icon and message
      var h = new qx.ui.layout.HorizontalBoxLayout;
      h.setWidth("80%");
      h.setHeight("auto");
      h.setVerticalChildrenAlign("middle");
      var i = new qx.ui.basic.Image(bigIcon);
      var a = new qx.ui.basic.Atom(msg);
      a.setWidth("1*");
      h.add(i, a);
      l.add(h);
 
      // controls array
      controls.forEach(function(control) {
        l.add(control);
      });
 
      qx.ui.core.ClientDocument.getInstance().add(w);
      w.show();
      return w;
    }
  }
});

You can use the new methods like so:

 
    // mixin providing windows for asynchronous user interaction (confirm, prompt, etc.)
    qx.Class.include(qx.application.Gui, custom.MUserInteraction );
  
    
    app = qx.core.Init.getInstance().getApplication(); 
    
    // simple information
    app.inform("This is a test");
    
    // user alert (warning)
    app.alert("This test is very bad",function(){
     alert("User acknowledged that this test is bad");
    },this);
    
    // confirmation
    app.confirm("Do you really want to do this?","Yes, do it","No, cancel", function(result){
      alert("User said " + ( result ? "'Yes'" : "'No'" ) );
    },this);
 
    // choices
    app.offer("Which fruit do you like best?",["Apples","Pears","Cherries","None of them"], function(result){
      alert("User chose '" + result + "'." );
    },this);
    
    // text field
    app.prompt("Please enter the name of your favorite GUI toolkit","qooxdoo",function(result){
      alert("User entered '" + result + "'." );
    }, this);
    
    // text area
    app.prompt("Please enter your private key","",function(result){
      alert("User entered '" + result + "'." );
    },this,5 );
 
    // form 
    var formData = {
	'database' : {
		'label' : "Database Name",
		'value' : "database1"	
	},
	'foo' : {
		'label' : "Bar",
		'value' : "BAZ!"	
	}
    };
    app.presentForm("Please enter database info", formData, function(formData){
      if ( ! formData  ) return;
      alert(formData.database.value);
    },this);
 

Information

Last modified:
2008/04/03 18:23 by c.boulanger

Account

Not logged in

 
 

Job Offers

To further improve qooxdoo we are seeking javascript developers. Read more...

Rich Ajax Platform (RAP)

RAP uses qooxdoo, Java and the Eclipse development model to build rich web applications. Read more...

qooxdoo Web Toolkit (QWT)

Similar to GWT this framework allows to create impressive qooxdoo applications just using Java. Read more...

Pustefix

Pustefix is a MVC-based web application framework using Java and XML/XSLT. Read more...

 
SourceForge.net Logo

Bad Behavior has blocked 0 potential spam attempts in the last 7 days.