|
The Gravey Framework and RATS RIA | |||||||
| PREV NEXT | FRAMES NO FRAMES | |||||||
Collection of model-view-controller support classes.
This library supports rich-internet-application code in the browser
using the Model-View-Controller design pattern (supported by Observer,
Command, Bean, Composite, etc. design patterns). A detailed explanation
of the functionality of this library can be found in the AJAX from Scratch
series of articles.
Version: 2.5
Requires:
| Class Summary | |
| MVCAJAXReplyCmd | This class implements the "process AJAX reply" command. |
| MVCAttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object. |
| MVCBoolEditController | This class manages a scalar editor with a set of yes,no,unknown radio buttons attached to the specified Boolean (i.e. |
| MVCBoolModel | This class encapsulates a Boolean scalar data model. |
| MVCButtonController | This class is a Controller that(1) watches a MVCBoolModel from which the "enable" state can be deduced (actually only uses MVCBoolModel.isTrue()), (2) manages a button view using the enable state, (3) defines a "pushed" event. |
| MVCChkBoxEditController | This class manages a form field "checkbox" editor of the specified MVCScalarModel. |
| MVCCmdButtonController | This class is a Button Controller for "undo/redo" buttons. |
| MVCCmdImgButtonController | This class is an Image Button Controller for "undo/redo" buttons. |
| MVCCollection | the MVCCollection "interface" has no code, only
an API followed by convention (ie DUCK-TYPING)
The required "interface" for "collections":
(1) getCount() - returns number of elements in collection
(2) getItem( itemKey ) - return specified element
(3) iterate( function(itemKey,itemObject,itemIndex) ) - calls
specified function on each element in collection;
If the function returns a true then the iteration
stops right there instead of continuing thru rest
of the elements in the collection
(4) getMemento() - return a "memento" that can be accepted later
(5) setMemento(m) - update the current state based on given "memento"
(6) reset() - empties collection of elements
(7) dump() - returns debug string with contents of collection
(8) iterate(func) - invokes func(itemKey,itemObject,itemIndex)
on each item in collection
See
Memento design pattern. |
| MVCCommand | This class acts as the abstract base class for each Command (ala Command design pattern). |
| MVCContext | This class encapsulates the criteria for building a MVCView such that if it changes, the view needs rebuilding. |
| MVCController | This class acts as the base class for each controller (that supports an underlying MVCView). |
| MVCDateEditController | This class manages a Date form field editor of the specified MVCScalarModel. |
| MVCDecode | This class encapsulates a code/decode (aka picklist) item. |
| MVCDequeModel | This class encapsulates a data model for Stacks and Queues. |
| MVCDollarEditController | This class manages an editor of the specified Dollar MVCScalarModel. |
| MVCDualBoolController | This class manages dual views (viewer and editor) of the specified (Boolean i.e. |
| MVCDualChkBoxController | This class manages dual views (viewer and editor) of the specified (boolean) Scalar Model. |
| MVCDualController | This abstract class manages dual views (viewer and editor) of a MVCScalarModel. |
| MVCDualDateController | This class manages dual views (viewer and editor) of the specified Date Scalar Model. |
| MVCDualDollarController | This class manages dual views (viewer and editor) of the specified Dollar Scalar Model. |
| MVCDualMenuController | This class manages dual views (viewer and popup menu) of the specified Models. |
| MVCDualStringController | This class manages dual views (viewer and editor) of the specified String Scalar Model. |
| MVCDualTextController | This class manages dual views (viewer and editor) of the specified "Text" Scalar Model. |
| MVCEditRule | This class encapsulates edit rules. |
| MVCFieldEditController | This class manages a form field editor of the specified MVCScalarModel. |
| MVCFormButtonController | This class is a Button Controller that manages a form button |
| MVCImgButtonController | This class is a ButtonController that manages an image button |
| MVCListModel | This class encapsulates a data model for a List of objects. |
| MVCListView | This class is a MVCView that expects to subscribe to a MVCListModel and will invoke itemHTMLstr() on each member of the list when buildHTMLstr() is called and itemPaint() on each member when paintHTML() is called. |
| MVCMap | This class encapsulates a Map of object/key pairs and implements the MVCCollection interface |
| MVCMapModel | This class encapsulates a data model for a Map of object/key pairs. |
| MVCMenuItem | the MVCMenuItem "interface" has no code, only
an API followed by convention (ie DUCK-TYPING)
The required "interface" for "menu items": (1) getDescription() - return "this" formatted for menu item |
| MVCModel | This abstract class acts as the base class for each MVC data model; Since models can subscribe to other models, they can act as both MVCObserver and MVCObservable. |
| MVCMutex | This class encapsulates a Map of mutual exclusion data;
It self-registers instantiations into a static Map;
This class implements
Lamport's bakery algorithm for mutual exclusion;
It is used to execute Command objects while making sure
that no other Command objects (that are using Mutex)
are executed at the same time. NOTE: our main use for this is to keep background AJAX processing from confusing foreground UI processing, which can otherwise occur because both are making data model changes simultaneously. |
| MVCObservable | the MVCObservable "interface" has no code, only
an API followed by convention (ie DUCK-TYPING)
The required "interface" for "observables": (1) addObserver( observer ) - add given MVCObserver to your list |
| MVCObserver | This class acts as the abstract base class for each "observer class" (ala Observer design pattern). |
| MVCPadScalarView | This class produces a blank-padded view of the specified MVCScalarModel. |
| MVCPopupMenuController | This class manages a popup menu which watches a MVCListModel (specifying the menu items) embedded within a MVCSelectionModel that reflects which item is/should-be currently selected. |
| MVCROAttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object. |
| MVCScalarEditCmd | This class implements a scalar edit command. |
| MVCScalarEditController | This abstract class manages an editor of the specified MVCScalarModel. |
| MVCScalarModel | This class encapsulates a Scalar data model with a default implementation of the scalar being implemented via a (bean-like) property. |
| MVCScalarView | This class produces a view of the specified MVCScalarModel. |
| MVCSelectionModel | This class encapsulates the data model for a selector which indicates the currently selected item in a specified MVCCollection data model. |
| MVCTextEditController | This class manages a form field "textarea" editor of the specified MVCScalarModel. |
| MVCTxtButtonController | This class is a Button Controller that manages a button that can display text based on a data model's current value |
| MVCUndoRedoModel | This class encapsulates the data model for the Command Dequeue which supports deep undo and redo. |
| MVCView | This class acts as the base class for each (MVC) View. |
| Method Summary | |
static boolean
|
mvcDoCmd( command )
Do the given command in "synchonized" mode (meaning that it will wait until commands that are already running/queued have finished). |
static String
|
mvcEmbedAttributeViewer( <MVCView> parentView, <String> attribute, <Function> optFormatter, <Object> optParam )
Create and embed, as a subview, a MVCScalarView of the specified data model attribute. |
static String
|
mvcEmbedBoolDualEditor( <MVCView> parentView, <String> attribute, <int> min, <String> className )
Create and embed, as a subview, a MVCDualBoolController of the specified data model attribute which is expected to be a (boolean) data element. |
static String
|
mvcEmbedChkBoxDualEditor( <MVCView> parentView, <String> attribute, <int> min, <String> className )
Create and embed, as a subview, a MVCDualChkBoxController of the specified data model attribute which is expected to be a (boolean) data element. |
static String
|
mvcEmbedDateDualEditor( <MVCView> parentView, <String> attribute, <String> className )
Create and embed, as a subview, a MVCDualDateController of the specified data model attribute which is expected to be a String data element containing a Date. |
static String
|
mvcEmbedDollarDualEditor( <MVCView> parentView, <String> attribute, <String> className, <Function> optFormatFunction )
Create and embed, as a subview, a MVCDualDollarController of the specified data model attribute which is expected to be a dollar amount data element. |
static String
|
mvcEmbedDualMenu( <MVCView> parentView, <String> attribute, <MVCSelectionModel> menuModel, <String> className, <String> optEvtHndlrName, <String> optInitDesc )
Create and embed, as a subview, a MVCDualMenuController of the specified data model attribute |
static String
|
mvcEmbedStringDualEditor( <MVCView> parentView, <String> attribute, <int> min, <int> max, <String> className, <Function> optKeyFilter )
Create and embed, as a subview, a MVCDualStringController of the specified data model attribute which is expected to be a String data element. |
static String
|
mvcEmbedTextDualEditor( <MVCView> parentView, <String> attribute, <int> rows, <int> cols, <int> max, <String> className )
Create and embed, as a subview, a MVCDualTextController of the specified data model attribute which is expected to be a String data element. |
static Object
|
mvcREDSPACER600()
|
static Object
|
mvcREDSPACER768()
|
static Object
|
mvcSECTIONBREAK()
|
static Object
|
mvcSPACER()
|
static void
|
onMVCFieldEditFocus( <event> e, <Element> field )
handle "entering a text field" event per webreference tip) |
static boolean
|
onMVCFieldEditKey( <event> e, <String> viewID )
handle "key pressed" events in text fields |
static Object
|
onMVCGlobalKeyPress(<event> e)
pre-screen all keypress events for entire page This implementation handles ctrl-z and ctrl-y to invoke Undo and Redo respectively. |
static void
|
onMVCRedoBtnPressed()
redo-button-pressed event handler |
static boolean
|
onMVCScalarEditUpdate( <event> e, <String> viewID )
ScalarEditController update-event handler |
static void
|
onMVCUndoBtnPressed()
undo-button-pressed event handler |
/////////////////////////////////////////////////////////////////////////// // This file uses JSDoc-friendly comments [ http://jsdoc.sourceforge.net/ ] // (JSDoc tutorial in book: "Foundations of AJAX", Chap 5) // TO BUILD DOCS: If ActivePerl and HTML::Templates are installed, // and JSDoc is installed at c:\JSDoc-1.9.8.1\jsdoc.pl // then execute buildDoc.bat and view jsdoc\index.html /////////////////////////////////////////////////////////////////////////// /** * @file grvMVC.js * @fileoverview Collection of model-view-controller support classes. * This library supports rich-internet-application code in the browser * using the Model-View-Controller design pattern (supported by Observer, * Command, Bean, Composite, etc. design patterns). A detailed explanation * of the functionality of this library can be found in the <a target="_blank" * href="http://www.polyglotinc.com/AJAXscratch/">AJAX from Scratch</a> * series of articles. * * @author Bruce Wallace (PolyGlotInc.com) * @requires grvUtils.js * @requires grvValidate.js * @requires grvClass.js * @version 2.5 */ ////////////////////////////////////////////////////////////////// // GRAVEY LEXICAL CODING CONVENTIONS: // (*) All private variables or functions start with "_" // (*) All variables and functions start with a lowercase letter // (*) All Classes start with an uppercase letter // (*) All Class methods and instance variables start with lowercase // (*) All Class "static" methods and variables start with uppercase // (*) All constants start with "k" // (*) All global variables start with "g" // (*) All event handler functions start with "on" // // (*) All Gravey utility global variables start with "gGrv" // (*) All Gravey MVC global variables start with "gMVC" // (*) All Gravey EDO global variables start with "gEDO" // (*) All Gravey utility functions start with "grv" // (*) All Gravey MVC functions start with "mvc" // (*) All Gravey MVC event handler functions start with "onMVC" // (*) All Gravey EDO event handler functions start with "onEDO" // (*) All Gravey MVC classes start with "MVC" // (*) All Gravey EDO classes start with "EDO" ////////////////////////////////////////////////////////////////// grvTraceCmp("grvMVC.js: Begin"); // Override these constants as needed var kMVCImgPath = "/images/"; //path to Gravey graphic files var kMVCDim_ = "DIM_"; //prefix to "dimmed" version of graphics var kMVCImgSpacer = 'spacer.gif'; //one pixel transparent var kMVCImgRedSpacer = 'redspacer.gif'; //one pixel red var kMVCImgCalendar = 'calendar.gif'; //calendar icon function mvcSPACER(){ return '<img src="'+kMVCImgPath+kMVCImgSpacer +'" height="1" width="5" alt="" align="middle"/>'; } function mvcSECTIONBREAK(){ return '<img src="'+kMVCImgPath+kMVCImgSpacer +'" height="30" width="600" alt="" align="middle"/>'; } function mvcREDSPACER600(){ return '<img src="'+kMVCImgPath+kMVCImgRedSpacer +'" height="20" width="600" alt="600 pixels wide" align="middle"/>'; } function mvcREDSPACER768(){ return '<img src="'+kMVCImgPath+kMVCImgRedSpacer +'" height="20" width="768" alt="768 pixels wide" align="middle"/>'; } /////////////////// GLOBAL VARIABLES ///////////////////// /** Global Undo Command Queue */ var gMVCUndoCmds; /** Global Root Container View */ var gMVCRootView; //////////////////////////////////////////// //////// PATTERN FOUNDATION CLASSES //////// //////////////////////////////////////////// /** * @class the MVCCollection "interface" has no code, only * an API followed by convention (ie <a target="_blank" * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>) *<pre> * The required "interface" for "collections": * (1) getCount() - returns number of elements in collection * (2) getItem( itemKey ) - return specified element * (3) iterate( function(itemKey,itemObject,itemIndex) ) - calls * specified function on each element in collection; * If the function returns a true then the iteration * stops right there instead of continuing thru rest * of the elements in the collection * (4) getMemento() - return a "memento" that can be accepted later * (5) setMemento(m) - update the current state based on given "memento" * (6) reset() - empties collection of elements * (7) dump() - returns debug string with contents of collection * (8) iterate(func) - invokes func(itemKey,itemObject,itemIndex) * on each item in collection *</pre> * See <a target="_blank" href="http://en.wikipedia.org/wiki/Mementto_pattern"> * Memento design pattern</a>. * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCCollection(){/* this function exists only for JsDoc purposes.*/} /** * @class the MVCMenuItem "interface" has no code, only * an API followed by convention (ie <a target="_blank" * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>) *<pre> * The required "interface" for "menu items": * (1) getDescription() - return "this" formatted for menu item *</pre> * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCMenuItem(){/* this function exists only for JsDoc purposes.*/} Class(MVCDecode,["Code Type","Code #","Code Description"]); /** * @class This class encapsulates a code/decode (aka picklist) item. * This class implements the {@link MVCMenuItem} interface. * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDecode() { /** @param {String} type type of code * @param {String} code value of code * @param {String} desc description of value * @param {String} aux auxiliary text */ this.konstructor = function( type, code, desc, aux ) { // define instance variables this.type = type; this.code = code; this.desc = desc; this.aux = aux; } /** return the description of "this" @type String */ this.getDescription = function() { return /*this.code + "-" + */ this.desc; } /** return the debug details of "this" @type String */ this.dump = function() { var dStr = new Array(); dStr.push( "Decode>>>[" ); dStr.push( "type=" +(this.type?this.type:"null") ); dStr.push( "code=" +(this.code?this.code:"null") ); dStr.push( "desc=" +(this.desc?this.desc:"null") ); dStr.push( " aux=" +(this.aix ?this.aux :"null") ); dStr.push( "]" ); return dStr.join(" "); } } Class(MVCMap); /** * @class This class encapsulates a Map of object/key pairs * and implements the {@link MVCCollection} interface * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCMap() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { // init instance variables this.name = optName || "unnamed Map"; this.map = new Object(); this.N = 0; } /** return a (deep) clone of "this" object. @type Map */ this.clone = function() { var m = new MVCMap( this.name ); this.iterate( function(K,o,i){ m.addItem(K,o); } ); return m; } /** return how many items are in map @type int */ this.getCount = function( ){ return this.N; } /** return item as string associated with given key @type String */ this.getItemStr = function( k ){ return this.map[k].toString(); } /** return item associated with given key @type Object */ this.getItem = function( k ){ return this.map[k]; } /** delete item associated with given key */ this.delItem = function( k ){ delete this.map[k]; --this.N; } /** add given object and associate with given key */ this.addItem = function(k,o){ this.map[k] = o; ++this.N; } /** reset map to empty */ this.reset = function( ){ this.map = new Object(); this.N = 0; } /** iterate thru items in map calling specified function * @param {Function} f function that takes key and object as params * and returns true if the iteration should be stopped before all * items in map are processed. */ this.iterate = function( f ) { var i = 0; for (k in this.map) if ( f( k, this.getItem(k), i++ ) ) return; //early } /** debug method to return this list as a string @type String */ this.dump = function() { var s = this.name + "=Map==>"; this.iterate( function( i, ithItem ){ s += "["+i+"]="+ ((ithItem.dump?ithItem.dump():ithItem.toString()) +"; "); } ); return s; } /** Return the key that comes after the given key. * If no key is specified, return the first key. * If no key matches the specs above, return null. */ this.nextKey = function( k ) { var nextKey = null; this.iterate( function(K,o,i){ if (!k) return nextKey = K;/*TRICKY!*/ if (k==K) k=null; } ); return nextKey; } /** Return the object that comes AFTER the given key * or, if no key specified, return the first object. * If no object fits the specs above, return null. */ this.next = function( k ) { var n = this.nextKey(k); return n ? this.getItem(n) : null; } /** return the first object in this map or null if empty @type Object */ this.first = function(){ return this.next(); } /** return the first key in this map or null if empty @type Object */ this.firstKey = function(){ return this.nextKey(); } /** return a memento of the current state of "this" @type Object */ this.getMemento = function(){ return this.clone(); } /** update "this" based on the given memento * @param {Object} m the memento */ this.setMemento = function(m) { this.reset(); var self = this; m.iterate( function(K,o,i){ self.addItem(K,o); } ); } } Class(MVCContext); /** * @class This class encapsulates the criteria for building * a {@link MVCView} such that if it changes, the view needs * rebuilding. This class is meant to be subclassed, however, * the default implementation implements a single-value context * where the value must be comparable via the "=" operator. * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCContext() { /** @param {anyComparableType} optContext optional value to save as view context */ this.konstructor = function( optContext ) { // init instance variables this.context = optContext; } /** * @param {MVCContext} lastBuildContext context to compare to THIS * @return whether the current context is the same as the given one * @type boolean */ this.sameAs = function( lastBuildContext ) { if (lastBuildContext==null) return false; return this.context == lastBuildContext.context; } /** return THIS formatted as string @type String */ this.toString = function(){ return "{"+this.context+"}"; } } // ------------------------ // --- OBSERVER PATTERN --- // ------------------------ /** * @class the MVCObservable "interface" has no code, only * an API followed by convention (ie <a target="_blank" * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>) *<pre> * The required "interface" for "observables": * (1) addObserver( observer ) - add given {@link MVCObserver} to your list *</pre> * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 * @see MVCModel */ function MVCObservable(){/* this function exists only for JsDoc purposes.*/} Class(MVCObserver); /** * @class This class acts as the abstract base class for each * "observer class" (ala Observer design pattern). *<pre> * Subclasses of MVCObserver should define/override: * (1) the {@link #update} method which accepts update events * and takes one parameter which is the "observable" * plus one optional adhoc parameter. *</pre> * @extends GrvObject * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCObserver() { this.konstructor = function() { // init instance variables this.model = null; //what do I watch (mostly, but not exclusively) } /** * override this method with your logic to respond to an update * event from one of the Observables you are subscribed to. * @param {MVCObservable} observable the generator of this update event * @param {Object} optAdhocObj optional adhoc object passed by sender */ this.update = function( observable, optAdhocObj ){ return; /*override me*/ } /** * subscribe to (aka watch/monitor/observe) the given observable * @param {MVCObservable} observable object to monitor */ this.subscribe = function( observable ) { this.model = observable; observable.addObserver( this ); } } // ----------------------- // --- COMMAND PATTERN --- // ----------------------- Class(MVCCommand); /** * @class This class acts as the abstract base class for each * Command (ala <a target="_blank" * href="http://en.wikipedia.org/wiki/Command_pattern"> * Command design pattern</a>). *<pre> * Subclasses of MVCCommand should define/override: * (1) the constructor to load the do/undo/redo context data * which should set the "valid" attribute to a negative * number if the command cant properly be initiated. * (2) the {@link #doit} method which executes the command * (3) the {@link #undo} method which "rolls back" the command * (4) the {@link #redo} method which "un-rolls-back" the command * (5) canUndo property if this cmd is only meant for mutex * * FYI, REDO would be different than DO, for example, in the case * that DO had to do a database search to get a value, but REDO * could simply use that saved value without re-searching for it. *</pre> * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCCommand() { // init static member variables if (grvIsUndefined(MVCCommand.NextID)) MVCCommand.NextID = 1; /** @param {String} cmdDesc general description of this command * @param {String} optErrMsg if not null then cancel this cmd */ this.konstructor = function( optName, optErrMsg ) { // init instance variables if (optErrMsg){ this.state = -1; alert(optErrMsg); return; } if (optName) this.name = optName; this.state = 0; //-1=invalid,0=notDone,1=done,2=undone,3=redone this.canUndo = true; this.id = MVCCommand.NextID++ } /** "Do command" logic */ this.doit = function(){ alert("DOIT:"+this); /*override me*/ } /** "command UNDO" logic */ this.undo = function(){ alert("UNDO:"+this); /*override me*/ } /** "command REDO" logic. Default is to just call {@link #doit} */ this.redo = function(){ this.doit(); /*override me*/ } /** return details of this command for user viewing @type String */ this.details = function(){ return ""; /*override me*/ } /** return THIS formatted as string @type String */ this.toString = function(){ return this.name + ": " + this.details(); } /** return true iff this command should not even be started @type boolean */ this.isInvalid = function(){ return this.state<0; } ///////////////// "synchronized" API ////////////////// /** synchronized call with global view blocking */ this.syncDo = function( methodName ) { gMVCRootView.block();//grvBreak("blocked to ["+View.Depth+"] for "+this); new MVCMutex( this, methodName ); gMVCRootView.unblock();//grvBreak("unblocked to ["+View.Depth+"] after "+this); } /** synchronized this.DOIT() */ this.syncDoIt = function(){ this.syncDo("DOIT"); } /** synchronized this.UNDO() */ this.syncUnDo = function(){ this.syncDo("UNDO"); } /** synchronized this.REDO() */ this.syncReDo = function(){ this.syncDo("REDO"); } //////////////// "unsynchronized" API ///////////////// /** "DO" this command if in the proper state. @throw error if in wrong state */ this.DOIT = function() { if (this.state!=0) throw "Cant DO an invalid or already started command."; gMVCRootView.block(); this.doit(); this.state = 1; gMVCRootView.unblock(); } /** "REDO" this command if in the proper state. @throw error if in wrong state */ this.REDO = function() { if (this.state!=2) throw "Cant REDO a command that isnt undone."; gMVCRootView.block(); this.redo(); this.state = 3; gMVCRootView.unblock(); } /** "UNDO" this command if in the proper state. @throw error if in wrong state */ this.UNDO = function() { if (!this.canUndo) throw "UNDO not supported for this command."; switch( this.state ) { case 1: case 3: gMVCRootView.block(); this.undo(); this.state = 2; gMVCRootView.unblock(); break; default: throw "Cant UNDO a command that isnt done."; } } } /** Do the given command in "synchonized" mode (meaning that * it will wait until commands that are already running/queued * have finished). NOTE: BECAUSE OF THIS, YOU WILL DEADLOCK * IF ANY COMMAND INVOKES ANOTHER COMMAND!!!!! * @return false if the command was invalid and not run * @type boolean * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcDoCmd( command ) { if ( command.isInvalid() ) return false; if (!command.canUndo) { gMVCUndoCmds.reset(); command.syncDoIt(); } else gMVCUndoCmds.newDo( command ); return true; } Class(MVCMutex,["command object","method name"]); /** * @class This class encapsulates a Map of mutual exclusion data; * It self-registers instantiations into a static Map; * This class implements <a target="_blank" * href="http://wikipedia.org/wiki/Lamport's_bakery_algorithm"> * Lamport's bakery algorithm</a> for mutual exclusion; * It is used to execute Command objects while making sure * that no other Command objects (that are using Mutex) * are executed at the same time.<p> * NOTE: our main use for this is to keep background AJAX * processing from confusing foreground UI processing, * which can otherwise occur because both are making data * model changes simultaneously. * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCMutex() { // init "static" Class methods/elements if (grvIsUndefined(MVCMutex.CpuSlice)){//keep this line above jsdoc comments! /** Static method to give a slice of CPU to mutex with given ID * @param {int} cmdID ID of command to resume * @param {int} optStartID optional ID of command on which we are waiting; * if not specified then start at top of list of all pending commands. */ MVCMutex.CpuSlice = function( cmdID, optStartID ) { // grvBreak("slice id="+cmdID+" start="+optStartID); MVCMutex.Map.getItem(cmdID).cpuSlice(optStartID); } // init static member variables MVCMutex.Map = new MVCMap("global mutex map"); } /** @param {Command} cmdObj Command object to be wrapped in Mutex * @param {String} methodName name of method being run on cmdObj * @param {boolean} optInhibitInvoke optional flag that if true * will inhibit immediate launching of cmdObj by this constructor */ this.konstructor = function( cmdObj, methodName, optInhibitInvoke ) { // init instance variables this.cmd = cmdObj; this.id = cmdObj.id; this.name = "Mutex for " + this.id; this.choosing = false; this.number = 0; this.methodID = methodName; // auto-register "this" MVCMutex.Map.addItem( this.id, this ); // auto start processing unless inhibited optInhibitInvoke || this.invoke(); } /** launch the processing of "this" mutex/command object */ this.invoke = function() { this.choosing = true; this.number = grvTimestamp(); this.choosing = false; MVCMutex.CpuSlice( this.id ); } /** continue the processing of "this" mutex/command object * @param {int} optStartID optional ID of last command we were waiting on; * if not specified then start at top of list of all pending mutex/commands. * @see #CpuSlice */ this.cpuSlice = function( optStartID ) { var startID = optStartID ? optStartID : MVCMutex.Map.firstKey(); for (var j=MVCMutex.Map.getItem(startID); j; j=MVCMutex.Map.next(j.id)) { if ( // delay if thread j still receiving its # j.choosing // delay if threads with smaller numbers (or with same #, // but with higher priority) still finishing their work || (j.number && (j.number < this.number || (j.number == this.number && j.id < this.id) ) ) ){ grvBusyDo( "MVCMutex.CpuSlice", '('+ this.id +','+ j.id +')', 10 ); return;//run away to fight another day (or millisecond) } } //by this point, we have exclusive access, so... // BEGIN CRITICAL SECTION... this.cmd[ this.methodID ](); //...END CRITICAL SECTION //end exclusive access this.number = 0; //since we are using cmd IDs instead of static thread numbers //(as is used in original bakery algorithm), we delete this //mutex to free memory. MVCMutex.Map.delItem( this.id ); } } Class(MVCEditRule); /** * @class This class encapsulates edit rules. STD edit rules:<pre> * MVCEditRule.kZero '0' Read-Only; zero * MVCEditRule.kCopy 'X' Read-Only; copy of legal balance (HACK!) * MVCEditRule.kRO 'R' Read-Only; (current value) * * MVCEditRule.kEdit '?' Read-Write; no validation * MVCEditRule.kReqd '*' Read-Write; non-empty required * MVCEditRule.kNonZ '#' Read-Write; non-zero required * * MVCEditRule.kPos '+' Read-Write; positive or zero * MVCEditRule.kPOS '{' Read-Write; positive or zero (but not empty) * MVCEditRule.kNeg '-' Read-Write; negative or zero * MVCEditRule.kNEG '}' Read-Write; negative or zero (but not empty) * MVCEditRule.kFone '(' Read-Write; phone number (or empty) * MVCEditRule.kFONE ')' Read-Write; phone number (required) * MVCEditRule.kZip 'z' Read-Write; zipcode (or empty) * MVCEditRule.kZIP 'Z' Read-Write; zipcode (required) * MVCEditRule.kAlfa 'a' Read-Write; letters[space] (or empty) * MVCEditRule.kALFA 'A' Read-Write; letters[space] (required) * MVCEditRule.kDec '.' Read-Write; decimal number (or empty) * MVCEditRule.kDEC ':' Read-Write; decimal number (required) * MVCEditRule.kInt '8' Read-Write; 0-9 (or empty) * MVCEditRule.kINT '9' Read-Write; 0-9 (required) * MVCEditRule.kBool 'y' Read-Write; Y,N,y,n,true,false (or empty) * MVCEditRule.kBOOL 'Y' Read-Write; Y,N,y,n,true,false (required) * MVCEditRule.kDate 'd' Read-Write; date (or empty) * MVCEditRule.kDATE 'D' Read-Write; date (required) * MVCEditRule.kAlNo ';' Read-Write; A-Za-z0-9 (or empty) * MVCEditRule.kALNO '|' Read-Write; A-Za-z0-9 (required) *</pre> * @extends GrvObject * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCEditRule() { // init "static" Class methods/elements if (grvIsUndefined(MVCEditRule.kEdit)){//keep this line above jsdoc comments! // init static member constants // NOTE: Dont EVER use blank as a rule! you are warned! // NOTE: Some of these constants must sync with GRAVE database! MVCEditRule.kZero = '0'; //R/O zero MVCEditRule.kCopy = 'X'; //R/O copy of legal balance MVCEditRule.kRO = 'R'; //R/O (current value) MVCEditRule.kEdit = '?'; //editable - no validation MVCEditRule.kReqd = '*'; //non-empty required MVCEditRule.kNonZ = '#'; //non-zero required MVCEditRule.kPos = '+'; //positive or zero MVCEditRule.kPOS = '{'; //positive or zero (but not empty) MVCEditRule.kNeg = '-'; //negative or zero MVCEditRule.kNEG = '}'; //negative or zero (but not empty) MVCEditRule.kFone = '('; //phone number (or empty) MVCEditRule.kFONE = ')'; //phone number (required) MVCEditRule.kZip = 'z'; //zipcode (or empty) MVCEditRule.kZIP = 'Z'; //zipcode (required) MVCEditRule.kInt = '8'; //0-9 (or empty) MVCEditRule.kINT = '9'; //0-9 (required) MVCEditRule.kDec = '.'; //decimal number (or empty) MVCEditRule.kDEC = ':'; //decimal number (required) MVCEditRule.kAlfa = 'a'; //A-Za-z[space] (or empty) MVCEditRule.kALFA = 'A'; //A-Za-z[space] (required) MVCEditRule.kBool = 'y'; //Y,N,y,n,true,false (or empty) MVCEditRule.kBOOL = 'Y'; //Y,N,y,n,true,false (required) MVCEditRule.kDate = 'd'; //date (or empty) MVCEditRule.kDATE = 'D'; //date (required) MVCEditRule.kAlNo = ';'; //A-Za-z0-9 (or empty) MVCEditRule.kALNO = '|'; //A-Za-z0-9 (required) /** Static method to return whether specified edit rule is readonly. * @param {char} rule rule to use * @type boolean */ MVCEditRule.IsReadOnly = function(rule) { switch( rule ) { case MVCEditRule.kZero: case MVCEditRule.kCopy: case MVCEditRule.kRO: return true; } return false; } /** Static method to validate value with STD edit rule. * @param {MVCScalarModel} model model whose current value to validate * @param {char} rule rule to apply * @return null if valid else error msg * @type String * @throw error if unknown rule is specified */ MVCEditRule.Validate = function( model, rule ) { var value = model.getValue(); if (value && value.toString().indexOf("~")>=0) return "The tilde (~) character is always illegal."; //pre-check required fields switch( rule ) { case MVCEditRule.kReqd: case MVCEditRule.kDATE: case MVCEditRule.kFONE: case MVCEditRule.kALNO: case MVCEditRule.kALFA: case MVCEditRule.kBOOL: case MVCEditRule.kINT: case MVCEditRule.kDEC: case MVCEditRule.kPOS: case MVCEditRule.kNEG: case MVCEditRule.kZIP: if (grvIsEmpty(value)) return "Value ["+value+"] must not be empty"; } //we assume at this point that missing required values are handled switch( rule ) { case MVCEditRule.kCopy: //no validation here--caller must handle copying value case MVCEditRule.kReqd: case MVCEditRule.kRO: case MVCEditRule.kEdit: //no extra validation at this point break; case MVCEditRule.kFONE: case MVCEditRule.kFone: if (grvIsPhoneNum(value)) break; return "Value ["+value+"] must be a phone number."; case MVCEditRule.kBOOL: case MVCEditRule.kBool: if (grvIsEmpty(value) || grvBoolValue(value)!=null) break; return "Value ["+value+"] must be 'Y/N' or 'true/false'."; case MVCEditRule.kDATE: case MVCEditRule.kDate: var errMsg = grvDateParseErrorMsg(value); if (errMsg==null) break; return "Value ["+value+"] must be a date..."+errMsg; case MVCEditRule.kZIP: case MVCEditRule.kZip: if (grvIsZipCode(value)) break; return "Value ["+value+"] must be a zipcode."; case MVCEditRule.kALNO: case MVCEditRule.kAlNo: if (grvIsAlphaNum(value)) break; return "Value ["+value+"] must contain only letters/digits."; case MVCEditRule.kALFA: case MVCEditRule.kAlfa: if (grvIsAlphaBlank(value)) break; return "Value ["+value+"] must contain only letters."; case MVCEditRule.kZero: //initial validation only--caller must handle setting value if (value==0) break; return "Value ["+value+"] must be Zero."; case MVCEditRule.kINT: case MVCEditRule.kInt: if (grvIsInteger(value)) break; return "Value ["+value+"] must be an integer."; case MVCEditRule.kDEC: case MVCEditRule.kDec: if (grvIsSignedFloat(value)) break; return "Value ["+value+"] must be a number."; case MVCEditRule.kPOS: case MVCEditRule.kPos: if (value>=0) break; return "Value ["+value+"] must be Positive."; case MVCEditRule.kNEG: case MVCEditRule.kNeg: if (value<=0) break; return "Value ["+value+"] must be Negative."; case MVCEditRule.kNonZ: if (value!=0) break; return "Must select a known (non-zero) value."; default: grvLoudThrow("Unknown MVCEditRule:["+rule+"]"); } //do this last! Give custom validation a shot if (model instanceof MVCAttributeModel) return model.validate(); //no errors found after all tests return null; } } /** no-op constructor...not used */ this.konstructor = function(){} } //////////////////////////////////////////// ///////////////// MODELS /////////////////// //////////////////////////////////////////// Class(MVCModel).Extends(MVCObserver); /** * @class This abstract class acts as the base class for each * MVC data model; Since models can subscribe to other models, * they can act as both {@link MVCObserver} and {@link MVCObservable}. * This class implements the {@link MVCObservable} interface. * @extends MVCObserver * @author Bruce Wallace (PolyGlotInc.com) * @version 2.1 */ function MVCModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.souper(); // init instance variables if (optName) this.name = optName; this.hasChanged = false; //only since last notifyObservers this.subscribers = new Array(); this.inXaction = false; this.updateStamp(); } /** debug method generating alert with subscriber list */ this.dumpSubscribers = function() { var N = this.subscribers.length; var s = this.name + " observed by["+N+"]: "; for (var i=0; i<N; ++i) s += (this.subscribers[i].name + "; "); grvBreak(s); } /** return "this" formatted as string @type String */ this.toString = function(){ return grvObjectToShortInitializer(this); } /** set the "dirty" flag to true */ this.dirty = function(){ this.hasChanged = true; this.updateStamp(); } /** clear the "dirty" flag that says "there has been a change to * this model since the last update event broadcast via publish()" */ this.clean = function(){ this.hasChanged = false; } /** tell observers that we have changed * @param {Object} optAdhocObj optional adhoc object to pass to observers */ this.publish = function( optAdhocObj ) { this.dirty(); if (!this.inXaction) this.notifyObservers( optAdhocObj ); } /** generic "bean" property GETTER */ this.GET = function(property ){ return this[property]; } /** generic "bean" property SETTER (w/o publish) */ this._SET = function(property,value){ this[property] = value; } /** generic "bean" property SETTER (w/publish) * @return flag saying if we published * @type boolean */ this.SET = function(property,value) { if (this.GET(property)==value) return false; this. _SET(property,value); this.publish(property); return true; } /** update the timestamp on this model */ this.updateStamp = function(){ this.timestamp = new Date().valueOf(); } /** inhibit publishing until matching EndTransaction() called */ this.BeginTransaction = function() { grvASSERT(!this.inXaction,"already started transaction"); // gMVCRootView.block(); this.inXaction = true; } /** publish a "batch" of updates */ this.EndTransaction = function() { grvASSERT(this.inXaction,"Not in transaction!"); this.inXaction = false; this.publish(); // gMVCRootView.unblock(); } /** {@link MVCObservable} API */ this.addObserver = function( observer ) { grvValidateArgs("MVCModel.addObserver",["observer"],arguments); this.subscribers.push( observer ); } /** @deprecated @throws not implemented exception */ this.delObserver = function( observer ) { grvValidateArgs ( "MVCModel.delObserver", ["observer"], arguments ); grvNotImplemented( "MVCModel.delObserver" ); //delete this.subscribers[observer.getID()]; } /** if "dirty" flag is set, Notify all subscribers of change * to our state; When done, if we were the initiator of the * cascade of update events (ie if global transaction depth * is back to zero when we are done), then the views will be redrawn. * @param {Object} optAdhocObj optional adhoc object passed to observers */ this.notifyObservers = function( optAdhocObj ) { if (this.hasChanged) { gMVCRootView.block(); var N = this.subscribers.length; for (var i=0; i<N; ++i) { //grvTraceMVC(this.name+"["+optAdhocObj+"] updates["+i+"] "+this.subscribers[i].name); this.subscribers[i].update( this, optAdhocObj ); } gMVCRootView.unblock(); } this.clean(); } } Class(MVCListModel).Extends(MVCModel); /** * @class This class encapsulates a data model for a List of objects. * This class implements the {@link MVCCollection} interface. * @extends MVCModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCListModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.souper( optName ); // init instance variables this._reset(); } /** return a (deep) clone of "this" object. @type MVCListModel */ this.clone = function() { var m = new MVCMap( this.name ); this.iterate( function(K,o,i){ m.addItem(K,o); } ); return m; } /** debug method to return this list as a string @type String */ this.dump = function() { var s = this.name + "=ListModel==>"; this.list.iterate( function( i, ithItem ){ s += ((ithItem.dump?ithItem.dump():ithItem.toString()) + "; "); } ); return s; } /** provide a "unique" number based on the state of this list * NOTE: the quality of this hash code depends on the quality * of the hash code provided by each list item. * @deprecated * @type int */ this.hash = function() { //this is a Q&D poor excuse for a hash algorithm var N = this.list.length; var h = 0; for (var i=0; i<N; ++i) h += (this.list[i].hash() * (2*(i+1))); return h; } /** pop top item off list but dont publish @return item @type Object */ this._pop = function( ){ return this.list.pop(); } /** push item onto list but dont publish @return index @type int */ this._push = function(o){ this.list.push(o); return this.getCount()-1; } /** delete given index from list @return deleted item @type Object */ this._del = function(i){ var o=this.list[i]; this.list.splice(i,1); return o; } /** clear list and update timestamp but dont publish */ this._reset = function( ){ this.list = new Array(); this.updateStamp(); return this; } /** clear list and publish */ this.reset = function( ){ this._reset(); this.publish(); return this; } /** return count of items in list @type int */ this.getCount = function( ){ return this.list.length; } /** return item in list with given index @type Object */ this.getItem = function(i){ return this.list[i]; } /** return item in list with given index as formatted string @type String */ this.getItemStr = function(i){ return this.list[i].toString(); } /** push given item onto list and publish @return index @type int */ this.addItem = function(o){ var i=this._push(o); this.publish(); return i; } /** delete given index from list and publish @return deleted item @type Object */ this.delItem = function(i){ var o=this._del(i); this.publish(); return o; } /** iterate thru items in list calling specified function * @param {Function} f function that takes index and object as params * and returns true if the iteration should be stopped before all * items in list are processed. */ this.iterate = function(f) { var N = this.getCount(); for (var i=0; i<N; ++i) if ( f( i, this.getItem(i), i ) ) return; //early } /** add the given object into the list just before the given zero-based-index */ this.addBefore = function(i,o){ this.list.splice(i,0,o); this.publish(); } /** return a memento of the current state of "this" @type Object */ this.getMemento = function() { var m = new Array(); for (var i=0; i<this.list.length; ++i) m[i] = this.list[i]; return m; } /** update "this" based on the given memento * @param {Object} m the memento */ this.setMemento = function(m) { this._reset(); for (var i=0; i<m.length; ++i) this.list[i] = m[i]; this.publish(); } } Class(MVCDequeModel).Extends(MVCListModel); /** * @class This class encapsulates a data model for Stacks * and Queues. The "stack" is built within a dequeue * such that up/down do not change the queue itself. * @extends MVCListModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDequeModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ){ this.souper( optName ); } /** clear the queue and reset the pseudo-stack but dont publish */ this._reset = function( ){ this.list = new Array(); this.tos = -1; //position in deque of top of pseudo-stack this.updateStamp(); } /** DEQUE API: add given object to back of the line @return item */ this.addFirst = function(o){ this.list.unshift(o); /*this.publish();*/ return o; } /** DEQUE API: add given object to front of the line @return item */ this.addLast = function(o){ this._push (o); /*this.publish();*/ return o; } /** DEQUE API: remove object from back of the line @return item */ this.delFirst = function( ){ var o = this.list.shift ( ); /*this.publish();*/ return o; } /** DEQUE API: remove object from front of the line @return item */ this.delLast = function( ){ var o = this._pop ( ); /*this.publish();*/ return o; } /** QUEUE API: add given object to back of the line @return item */ this.enqueue = function(o){ return this.addFirst(o); } /** QUEUE API: remove object from front of the line @return item */ this.dequeue = function( ){ return this.delLast ( ); } /** STACK API: push given object to front of the line aka top of the stack @return item */ this.push = function(o){ this.tos = this.getCount() ; return this.addLast(o); } /** STACK API: remove given object from front of the line aka top of the stack @return item */ this.pop = function( ){ this.tos = this.getCount()-1; return this.delLast( ); } /** pseudo-Stack API: return the top of the pseudo-stack */ this.top = function(offset){ if (offset) offset = parseInt(offset); else offset = 0; return this.getItem( offset+this.tos ); } /** pseudo-Stack API: return index of next up iff we can go up */ this.upIndex = function(){ return ((this.getCount()-this.tos)<=1) ? null : this.tos+1; } /** pseudo-Stack API: non-destructive push/get */ this._up = function(){ ++this.tos; return this.top(); } /** pseudo-Stack API: return index of next down iff we can go down */ this.downIndex = function(){ return (this.tos<0) ? null : this.tos; } /** pseudo-Stack API: non-destructive pop */ this._down = function(){ --this.tos; return this.top(1); // return what we "popped" } /** pseudo-Stack API: throw away items above top of pseudo-stack */ this._cutback = function(){ while (this.getCount()>(this.tos+1)) this._pop(); } } Class(MVCMapModel).Extends(MVCModel); /** * @class This class encapsulates a data model for a Map of * object/key pairs. Its API is a wrapper for the {@link MVCMap} API. * This class implements the {@link MVCCollection} interface. * @extends MVCModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCMapModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.souper( optName ); // init instance variables this.map = new MVCMap("Map for :"+this.name); } this.getCount = function( ){ return this.map.getCount(); } this.getItemStr = function(k ){ return this.map.getItemStr(k); } this.getItem = function(k ){ return this.map.getItem(k); } this.delItem = function(k) { this.map.delItem(k); this.publish(); } this.addItem = function(k,o){ this.map.addItem(k,o); this.publish(); } this.reset = function( ){ this.map.reset(); this.publish(); } this.iterate = function(f) { this.map.iterate(f); } /** debug method to return this list as a string @type String */ this.dump = function() { return this.name + "=MapModel==>" + this.map.dump(); } /** return a memento of the current state of "this" @type Object */ this.getMemento = function(){ return this.map.getMemento(); } /** update "this" based on the given memento * @param {Object} m the memento */ this.setMemento = function(m){ this.map.setMemento(m); this.publish(); } } Class(MVCScalarModel).Extends(MVCModel); /** * @class This class encapsulates a Scalar data model with a * default implementation of the scalar being implemented via * a (bean-like) property. The property name can optionally * be specified.<p> * Scalar models assume that a validity attribute of the * basic model value can also be set and it's member name * is based on the name of the basic value's member name. * By convention, the validity attribute consists of an error * message string if invalid or null if valid. * @extends MVCModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCScalarModel() { // init "static" Class methods/elements if (grvIsUndefined(MVCScalarModel.kValiditySuffix)){//keep this line above jsdoc comments! /** The suffix added to the basic name to get the validity attribute name. */ MVCScalarModel.kValiditySuffix = ".err"; } /** @param {String} optPropName optional property name to use instead of default * @param {String} optName optional name of this instance */ this.konstructor = function( optPropName, optName ) { this.souper( optName ); // init instance variables this.pname = optPropName || "scalarvalue"; } /** set scalar to given value but dont publish */ this._setValue = function(x){ this._SET(this.pname,x); } /** set scalar to given value and publish */ this.setValue = function(x){ return this. SET(this.pname,x); } /** get scalar value */ this.getValue = function( ){ return this. GET(this.pname ); } /** set the validity attribute of this scalar value and dont publish * @param {String} errMsg the validity attribute (as an error message) */ this._setValidity = function( errMsg ){ this._SET( this.pname+MVCScalarModel.kValiditySuffix, errMsg ); } /** return the validity attribute of this scalar value @type String */ this.getValidity = function(){ return this. GET( this.pname+MVCScalarModel.kValiditySuffix ); } } Class(MVCBoolModel).Extends(MVCScalarModel); /** * @class This class encapsulates a Boolean scalar data model. * @extends MVCScalarModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCBoolModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ){ this.souper( "boolflag", optName ); } /** return the current value of this model */ this.isTrue = function(){ return this.getValue(); } /** set model to given value and publish * @param {boolean} b value to set model to */ this.setFlag = function(b){ this.setValue(b); } } Class(MVCAttributeModel,["base object","attribute"]).Extends(MVCScalarModel); /** * @class This class encapsulates a "wrapper" data model for a * specified attribute of a specified base object. * The specified attribute can NOT be a method call. * @extends MVCScalarModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCAttributeModel() { // init "static" Class methods/elements if (grvIsUndefined(MVCAttributeModel.kValidateSuffix)){//keep this line above jsdoc comments! /** The suffix added to the basic name to get the validate function name. */ MVCAttributeModel.kValidateSuffix = ".vfunc"; } /** @param {Object} baseObject object whose property we are wrappering * @param {String} attribute name of baseObject's member we are wrappering * @param {String} optName optional name of this instance */ this.konstructor = function( baseObject, attribute, optName ) { this.souper( undefined, optName?optName:attribute ); // init instance variables this.attribute = attribute; this.base = baseObject; } /** set the value of base object attribute and publish */ this.setValue = function(x) { this._setValue( x ); this.publish(); } /** set the value of base object attribute but dont publish */ this._setValue = function(x){ this.base[ this.attribute ] = x; } /** return the value of base object attribute */ this.getValue = function( ){ return this.base[ this.attribute ]; } /** set the validity attribute of this attribute but dont publish * @param {String} errMsg the validity attribute (as an error message) */ this._setValidity = function( errMsg ){ this.base[ this.attribute+MVCScalarModel.kValiditySuffix ] = errMsg; } /** return the validity attribute of this attribute @type String */ this.getValidity = function(){ return this.base[ this.attribute+MVCScalarModel.kValiditySuffix ]; } /** return error message from custom validation method or null for no error */ this.validate = function() { if (this.base && this.base[ this.attribute+MVCAttributeModel.kValidateSuffix ]) { return this.base[ this.attribute+MVCAttributeModel.kValidateSuffix ]( this.attribute ); } else return null; } } Class(MVCROAttributeModel,["base object","attribute"]).Extends(MVCAttributeModel); /** * @class This class encapsulates a "wrapper" data model for a * specified attribute of a specified base object. * The specified attribute can in fact be a method name and * it will be called as needed. This model is READ-ONLY. * @extends MVCAttributeModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCROAttributeModel() { /** @param {Object} baseObject object whose property we are wrappering * @param {String} attribute name of baseObject's member we are wrappering * @param {String} optParam optional parameter to pass to attribute if * it is a method * @param {String} optName optional name of this instance */ this.konstructor = function( baseObject, attribute, optParam, optName ) { this.souper( baseObject, attribute, optName ); // init instance variables this.optParam = optParam; } /** invoke our attribute as a method call and return result */ this.invoke = function(f,optParam){ return f.call(this.base,optParam); } this._setValue = function(){ grvError("attempt to set a read-only model"); } this._setValidity = function(){ this._setValue(); } this.setValue = function(){ this._setValue(); } /** return current value of base object attribute (even if it is a method). */ this.getValue = function(){ var x = this.base[ this.attribute ]; if (x && x instanceof Function) return this.invoke(x,this.optParam); return x; } } Class(MVCSelectionModel,["Collection Model"]).Extends(MVCScalarModel); /** * @class This class encapsulates the data model for a selector * which indicates the currently selected item in a specified * MVCCollection data model. This means that the range of legal values * for this model's value is [0..Collection.getCount()-1] plus * a "nothing selected" value. * @extends MVCScalarModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCSelectionModel() { // init "static" Class methods/elements if (grvIsUndefined(MVCSelectionModel.kNothingSelected)){ // init static member variables MVCSelectionModel.kNothingSelected = -1; MVCSelectionModel.kPropertyIdSelect = 'selected'; } /** @param {Object} collModel Collection data model we select from * @param {String} optName optional name of this instance */ this.konstructor = function( collModel, optName ) { this.souper( MVCSelectionModel.kPropertyIdSelect, optName ); // init instance variables this._selectNothing(); this.subscribe( collModel ); } /** return our Collection model @type Collection */ this.getList = function( ){ return this.model; } /** return the specified item in our Collection * @param {Object} i item key */ this.getItem = function(i){ return this.getList().getItem(i); } /** return the size of our MVCCollection model */ this.getCount = function( ){ return this.getList().getCount(); } /** return whether there is currently a selection @type boolean */ this.hasSelection = function( ){ return this.getValue()!=MVCSelectionModel.kNothingSelected; } /** return the currently selected MVCCollection item or undefined if nothing selected */ this.getSelection = function( ){ return this.getItem( this.getValue() ); } /** return the currently selected MVCCollection item formatted as string @type String */ this.getSelectionStr = function( ){ return this.getSelection().toString(); } /** return the specified MVCCollection item formatted as description @type String */ this.getDescription = function(i){ var item = this.getItem(i); return item ? item.getDescription() : 'Unknown Value "'+i+'"'; } /** select "nothing" but dont publish */ this._selectNothing = function() { this._setValue( MVCSelectionModel.kNothingSelected ); } /** select "nothing" and publish */ this.selectNothing = function(){ this._selectNothing(); this.publish(); } /** select the specified Collection item but dont publish * @param {Object} k key of item to select * @param {boolean} optLenient optional flag to allow out of range to selectNothing */ this._select = function(k,optLenient) { if ( k!=0 && this.getIndex(k)<0 ) { if (!optLenient) grvError("BAD SelectionModel KEY["+k+"]"); this._selectNothing(); } else this._setValue(k); } /** select the specified MVCCollection item and publish * @param {Object} k key of item to select * @param {boolean} optLenient optional flag to allow out of range to selectNothing */ this.select = function(k,optLenient){ this._select(k,optLenient); this.publish(); } /** select the zero-th item (NOT "nothing selected") but dont publish */ this._unselect = function( ){ this._select(0); } /** select the zero-th item (NOT "nothing selected") and publish */ this.unselect = function( ){ this._unselect(); this.publish();} /** re-select the current value and publish */ this.reselect = function( ){ this.publish(); } /** handle MVCCollection model update event by "unselect"ing */ this.update = function( ){ if ( this.getList().hasChanged ) this.unselect(); //grvTraceEvt("UnSelectUpdate["+this+"]"); } /** return the index into the MVCCollection model of the specified key * (or the current selection if no key specified) * @return -1 if not found * @type int */ this.getIndex = function( optKey ) { var index = -1; var goal = optKey ? optKey : this.getValue(); this.model.iterate( function(key,o,i){ if (key==goal){ index = i; return true; } } ); return index; } /** return a memento of the current state of "this" @type Object */ this.getMemento = function() { var m = new Object(); m.list = this.getList().getMemento(); m.val = this.getValue(); return m; } /** update "this" based on the given memento * @param {Object} m the memento */ this.setMemento = function(m) { this.getList().setMemento( m.list ); this.select( m.val, true ); } } Class(MVCUndoRedoModel).Extends(MVCDequeModel); /** * @class This class encapsulates the data model for the * Command Dequeue which supports deep undo and redo. * NOTE: This logic invokes the "synchronized" version * of the MVCCommand API. * @extends MVCDequeModel * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCUndoRedoModel() { this.konstructor = function(){ this.souper( "undo/redo commands" ); } /** Add a new command object to the top of the "stack" and "do" it. */ this.newDo = function(c){ this._cutback(); {this.push(c).syncDoIt(); this.publish();} } /** "pop the top" command object and "undo" it. */ this.unDo = function( ){ if (this.downIndex()!=null) {this._down().syncUnDo(); this.publish();} } /** "unpop the top" command object and "redo" it. */ this.reDo = function( ){ if (this. upIndex()!=null) {this. _up().syncReDo(); this.publish();} } /** iff command index is defined, return command description @type String */ this.cmdDesc = function(i){ return (i!=null) ? this.getItem(i).toString() : null; } /** iff there is another undo-able command return its description @type String */ this.hasUnDo = function(){ return this.cmdDesc( this.downIndex() ); } /** iff there is another redo-able command return its description @type String */ this.hasReDo = function(){ return this.cmdDesc( this. upIndex() ); } } /** Global Undo Command Queue */ gMVCUndoCmds = new MVCUndoRedoModel(); //////////////////////////////////////////// ///////////////// VIEWS /////////////////// //////////////////////////////////////////// Class(MVCView); /** * @class This class acts as the base class for each (MVC) View. *<p> * Views are responsible for keeping up-to-date the HTML * associated with a particular portion of the web page * identified via a "hook" (i.e. an HTML element ID). *<p> * The view should display the current state of the data * in the model(s) that it "watches". [NOTE: Views do not * "subscribe" to Models and react to their individual update * events because all views need to draw in a coordinated * top-down fashion.] *<p> * Each View is also a container of subviews (as needed) * and coordinates their layout by managing some skeleton * framework HTML (e.g. tables/divs/spans/etc) to which * the subviews hook and manage [I.E. the GoF <a target="_blank" * href="http://www.javaworld.com/javaworld/jw-09-2002/jw-0913-designpatterns_p.html"> * Composite design pattern</a>]. *<p> * Once set up, views just react to "draw" events where * they draw "this" view and then recurse thru any subviews * invoking their draw method. [This is so that any elements * of this view that are to be "hooks" for any subviews can * be generated by this view first.] *<p> * Whenever a {@link MVCController} event causes some {@link MVCModel} * to change, (and after all observing data models have finished * their updates), the global "root container" view will initiate * a single draw event cascade to update all Views on the page. *<p> * "Drawing" entails first looking at the appropriate data * model(s) for this view and deciding whether they require * "rebuilding" the HTML of this view, and if so, replacing * the current HTML with newly generated HTML [via the innerHTML * of the HTML Element ID associated with this view]. * Then the {@link #paint} method is invoked to "decorate" the view * with the model(s) current data (i.e. set any HTML attributes * that need updating e.g. background color). Normally, HTML * need not be constantly rebuilt, only decorated. * A special case is where a container needs to rebuild its * HTML, all subviews are forced to as well (even if they * wouldnt normally based on the views they are watching). * <p> * The HTML (in string form) is generated by the abstract method * "buildHTMLstr". The abstract method "mustRebuild" decides * whether the draw event requires the HTML to be rebuilt before * calling "paintHTML" (which is called if "mustRepaint"). *<p><pre> * Subclasses of View should define/override: * (1) {@link #buildHTML} constructs this view's HTML * OR, use the default buildHTML which builds, via innerHTML, the * HTML string returned from buildHTMLstr(), hence you would * override instead: * (1) {@link #buildHTMLstr} generates this view's HTML (as string) * * (2) {@link #paintHTML} modifies/decorates existing HTML structures * (3) {@link #mustRepaint} decides if this view's HTML needs repainting * (4) {@link #mustRebuild} decides if this view's HTML needs rebuilding * The default implementation of mustRebuild() requires that * instead of overriding mustRebuild() instead override: * (4) {@link #currentContext} returns an MVCContext subclass object * containing the driver information for building this view. *</pre> * @extends GrvObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCView() { // init "static" Class methods/elements if ( grvIsUndefined(MVCView.Depth) ) {//keep this line above jsdoc comments! /** Static method to draw the root view (and hence all views) */ MVCView.DrawRoot = function() { gMVCRootView.draw(); } //init static member variables MVCView.Depth = 0; } /** @param {boolean} optDisable optional "disable view" flag (default false) * @param {String} optName optional name of this instance * @param {String} optViewID optional ViewID of this instance. * Note that while all views must have a view ID, * they are usually assigned an ID at the point where * a view is added as a subview to another view * via {@link #addSubView}, {@link #embedView}, etc. */ this.konstructor = function( optName, optViewID, optDisable ) { // init instance variables if (optName) this.name = optName; this.disabled = optDisable || false; this.visible = true; this.model = null; //default (but not necessarily only) model this.subviews = new MVCMap( this.name+" subviews" ); this.parentView = null; this.embeddedSubViews = false; //if true subviews are built via buildHTMLstr() this.lastBuildContext = null; this.setViewID( optViewID ); } /** return an HTML string of the basic structure for this view. * As a side effect, build/update subview list for this view. * As a side effect, invoke this method on each nested subview. * @return HTML (suitable for assigning to innerHTML) * @type String */ this.buildHTMLstr = function(){ return ""; /*override me*/ } /** update/modify attributes of basic existing HTML for THIS view */ this.paintHTML = function(){ /*override me*/ } /** return true IFF model(s) require the HTML rebuilt for THIS view * default implementation: If there is a difference in the current * "build context" and the current one then rebuild else not. * @type boolean */ this.mustRebuild = function(){ return this.contextChanged(); } /** return true IFF model(s) require HTML repaint. * NOTE: rebuilding view forces repaint of all subviews. * NOTE: repainting THIS view does NOT force repaint of subviews. * @type boolean */ this.mustRepaint = function(){ return true; /*override me if needed*/ } /** return the current "context" of this view. * Default is "no rebuild ever needed" (unless others force us to) * @type MVCContext */ this.currentContext = function(){ return new MVCContext( 0 ); /*override me*/ } /** register the given model as our primary data model * @param {MVCModel} m the data model to "watch" */ this.watchModel = function(m ){ this.model = m; } /** (re)define the view ID for this view */ this.setViewID = function(ID){ this.viewID = ID; this.hook = null; this.widget = null; } /** return the "inner" view ID for this view @type String */ this.innerID = function(){ return this.viewID + ".inner"; } /** Return the effective ID of the widget for this view. * Finesse the fact that the viewID is sometimes * the ID of a wrapper tag (e.g. span) and other * times is the ID of the actual view's tag. * We assume that views that are "embedded" have * no wrapper tag, otherwise they do. In any event, * the MVCView subclass can say what the "inner" ID * is when there is a wrapper. * @type String */ this.getWidgetID = function( optAttributeID ) { var noWrapper = this.parentView && this.parentView.embeddedSubViews; var id = noWrapper ? this.viewID : this.innerID(); return id + (optAttributeID ? "."+optAttributeID : ""); } /** return the HTML element of this view's widget tag @type element */ this.getWidget = function(forceReload) // cached lazy load { if (this.widget==null || forceReload) this.widget = grvGetHook( this.getWidgetID() ); return this.widget; } /** return the HTML element of this view's hook tag @type element */ this.getHook = function(forceReload) // cached lazy load { if (this.hook==null || forceReload) this.hook = grvGetHook( this.viewID ); return this.hook; } /** Build the HTML for this view. This can be overrided to directly * build HTML via DOM operations or use this default implementation * that takes an HTML string from {@link #buildHTMLstr} and puts it * into the innerHTML of this view's hook HTML element. Note that * building the HTML for this view implies rebuilding the HTML for * all "embedded" subviews. */ this.buildHTML = function() { var hook = this.getHook(true); if (hook!=null){ if (this.embeddedSubViews) this.clearSubViews(); hook.innerHTML = this.buildHTMLstr(); } } /** update and save the current {@link MVCContext} for this view */ this.updateContext = function(){ this.lastBuildContext = this.currentContext(); } /** return true iff the view context has changed @type boolean */ this.contextChanged = function(){ return ! this.currentContext().sameAs( this.lastBuildContext ); } /** disable this view (and hence all subviews) */ this.disable = function( ){ this.disabled = true; } /** enable this view (thereby enabling all enabled subviews) */ this.enable = function( ){ this.disabled = false; } /** set this view as visible or not and manifest it via the HTML. * If setting to invisible then we also set all subviews to invisible * BUT NOT THE OTHER WAY ROUND! */ this.setVisible = function(isVisible) { //fix problem where tooltips quit working switching to WinXP grvSetElemDisplay( this.getWidget (), this.visible=isVisible );//TRICKY // grvSetVisibility ( this.getWidgetID(), this.visible=isVisible );//TRICKY if (!isVisible) this.setSubViewsVisible(false); } /** set all subview visiblility */ this.setSubViewsVisible = function(v){ this.subviews.iterate( function(viewID,aView){aView.setVisible(v)} ); } /** return whether this view is visible @type boolean */ this.isVisible = function(){ return this.visible; } /** if enabled, force a "draw" of this view (and all enabled subviews) */ this.redraw = function(){ this.draw(true); } /** if enabled, cause entire subview tree to be built/painted as needed */ this.draw = function( optForceRebuild ) { if (this.disabled) return; //grvBreak("draw ["+this.name+"] depth="+MVCView.Depth); //Recursively (re)build any view in the //entire subview tree that needs building. var rebuilt = this.build( optForceRebuild ); //subview tree should be stable now, so, //recursively (re)paint entire subview tree this.paint( rebuilt ); } /** does this view or any embedded subview need rebuilding? @type boolean */ this.rebuildAny = function() { var must = this.mustRebuild(); if (!must) //no need to check if we already know we need to build if (this.embeddedSubViews) this.subviews.iterate( function(vID,vw){ if (vw.rebuildAny()) return must = true;/*TRICKY!*/ } ); // if (must) grvTraceMVC(this.name+" says must rebuild"); return must; } /** Recursively build this view and entire subview tree. * @return whether rebuild was done. * @type boolean */ this.build = function( optForceRebuild ) { if (this.disabled) return false; //grvBreak("build ["+this.name+"] depth="+MVCView.Depth); var rebuild = optForceRebuild || this.rebuildAny(); //grvTraceEvt("BUILD: "+this.name+"[rebuild="+rebuild+"] ID="+this.viewID); if (rebuild) { this.updateContext(); this.buildHTML(); } if (!this.embeddedSubViews) this.buildsubviews( rebuild ); return rebuild; } /** Recursively invoke build on entire subview tree. * @return whether rebuild was done. * @type boolean */ this.buildsubviews = function( optForceRebuild ) { this.subviews.iterate( function( viewID, aView ){ aView.setViewID( viewID ); aView.build( optForceRebuild ); } ); } /** Recursively invoke paint on entire subview tree. */ this.paint = function( optForceRepaint ) { if (this.disabled) return; //grvBreak("paint ["+this.name+"] depth="+MVCView.Depth); if (optForceRepaint || this.mustRepaint()) this.paintHTML(); this.subviews.iterate( function( viewID, aView ){ aView.paint(); } ); } /////////// CONTAINER INTERFACE ////////////// // NOTE: Changing the subviews list does not cause a redraw // so changes will not appear until the next global draw. /** set the parent view of this view */ this.setParentView = function(v){ this.parentView = v; } /** clear the list of subviews of this view */ this.clearSubViews = function( ){ this.subviews.reset(); } /** add the specified view/ID to our subview list */ this.addSubView = function( viewID, aView ) { aView.setViewID( viewID ); aView.setParentView( this ); this.subviews.addItem( viewID, aView ); } /** find the specified view in the tree of subviews @type MVCView*/ this.getSubView = function( viewID ) { var theView = this.subviews.getItem( viewID ); if (!theView) this.subviews.iterate( function( aViewID, aView ){ var v = aView.getSubView( viewID ); if (v) {theView = v; return true;} } ); return theView; } /** delete the specified view from our subview list */ this.delSubView = function( viewID ){ this.subviews.delItem( viewID ); } /** add the specified view/ID as an "embedded" subview. Embedded subviews * are those whose HTML string is embedded in the HTML string of its * parent view. I.E. When {@link #buildHTMLstr} is called for a view, * it is expected to return a string containing its HTML and all the * HTML for its embedded subviews. NOTE: If one subview is embedded * then ALL subviews of this view must be embedded. * @return view * @type MVCView */ this.embedView = function( viewID, view ) { this.embeddedSubViews = true; this.addSubView( viewID, view ); view.updateContext(); return view; } /** same as {@link #embedView} but return the HTML string of the * view rather than the View object. * @return HTML string * @type String */ this.embedHTML = function( viewID, view ){ return this.embedView( viewID, view ).buildHTMLstr(); } /** same as {@link #embedHTML} but auto-creates viewID and saves reference * to view based on specified "attribute" ID. * @param {MVCView} view the attribute's subview to embed * @param {String} attrID the identifier of the attribute * @return HTML string * @type String */ this.embedAttr = function( attrID, view ) { var viewID = this.getWidgetID(attrID); this[attrID] = view; //squirrel away reference to view return this.embedView( viewID, view ).buildHTMLstr(); } // NOTE: DONT! call grvBusy() in block because it causes the screen // to flash on menu selections!?! It took hours to track it down... // You are warned! /** API to block/unblock view updating (to stop redraw thrashing) */ this.block = function(){ ++MVCView.Depth; //grvBreak("++depth="+MVCView.Depth); } /** API to block/unblock view updating (to stop redraw thrashing) */ this.unblock = function(){ if (--MVCView.Depth <= 0) { //grvBreak("--depth="+MVCView.Depth); MVCView.Depth = 0; MVCView.DrawRoot(); //grvBreak("finished rootdraw depth="+MVCView.Depth); } } } /** Global Root Container View (initialized to empty container) */ gMVCRootView = new MVCView( "root view", "fauxRootHook", true ); Class(MVCListView).Extends(MVCView); /** * @class This class is a MVCView that expects to subscribe to a * {@link MVCListModel} and will invoke {@link #itemHTMLstr} on * each member of the list when {@link #buildHTMLstr} is called * and {@link #itemPaint} on each member when {@link #paintHTML} * is called. *<p><pre> * Subclasses of MVCListView should define/override: * (A) {@link #itemHTMLstr} which creates HTML for specified item * (B) {@link #itemPaint} which decorates HTML for specified item * and optionally... * (C) {@link #headHTMLstr} which creates HTML for a header item * (D) {@link #headPaint} which decorates HTML for a header item *</pre> * @extends MVCView * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCListView() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.souper( optName ); //each list element view is effectively an implicit embedded view, //hence dont try to build them again via buildsubviews()! this.embeddedSubViews = true; } /** method that should return HTML string for specified list item * @param {int} index index into our MVCListModel * @param {Object} item the actual item from our MVCListModel * @param {String} itemID the view ID of the corresponding item subview * @type String */ this.itemHTMLstr = function(index,item,itemID){ /* abstract */ } /** method that should decorate HTML for specified item * @param {int} index index into our MVCListModel * @param {Object} item the actual item from our MVCListModel * @param {String} itemID the view ID of the corresponding item subview */ this.itemPaint = function(index,item,itemID){ /* abstract */ } /** method that should return HTML string for the header item * @type String */ this.headHTMLstr = function(){ /* abstract */ return ""; } /** method that should decorate HTML for the header item */ this.headPaint = function(){ /* abstract */ } /** return the view ID for the item subview specified * @param {int} index index into our MVCListModel/MVCListView * @type String */ this.itemViewID = function(index){ return this.getWidgetID() + index; } /** invoke {@link #itemPaint} for each item in our list */ this.paintHTML = function() { this.headPaint(); var listView = this; this.model.iterate( function( i, ithItem ){ listView.itemPaint( i, ithItem, listView.itemViewID(i) ); } ); } /** return the combined HTML string built from each {@link #itemHTMLstr} * @type String */ this.listHTMLstr = function() { var HTML = new Array(); HTML.push( this.headHTMLstr() ); var listView = this; this.model.iterate( function( i, ithItem ){ HTML.push( listView.itemHTMLstr( i, ithItem, listView.itemViewID(i) ) ); } ); return HTML.join(''); } /** generate container/framework HTML @type String */ this.buildHTMLstr = function() { var HTML = new Array(); //HTML.push( '<table>' ); //HTML.push( '<tbody>' ); HTML.push( this.listHTMLstr() ); //HTML.push( '</tbody>' ); //HTML.push( '</table>' ); return HTML.join(''); } } Class(MVCScalarView,["Scalar Model"]).Extends(MVCView); /** * @class This class produces a view of the specified * {@link MVCScalarModel}. It accepts an optional formatter * function specification that will transform the raw value * of the model into a desired format. This formatter * function should accept one parameter (the value) and * return a formatted string version of that value. * @extends MVCView * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCScalarView() { /** @param {MVCScalarModel} xModel the data model to view * @param {Function} optFormatFunction optional formatter function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, optFormatFunction, optName ) { this.souper( optName ); // init instance variables this.watchModel( xModel ); this.formatter = optFormatFunction; } /** return the formatted version of the given value using our formatter * function (where the raw value is returned if no formatter is registered) * @type String */ this.formatted = function(x){ return this.formatter ? this.formatter(x) : x; } /** return the current value of our model formatted with our formatter @type String */ this.getValueStr = function( ){ return this.formatted( this.model.getValue() ); } /** set the value of our display "widget" */ this.setDisplay = function(x){ this.getWidget().innerHTML = x; } /** update the display with the current model value */ this.updateView = function( ){ this.setDisplay( this.getValueStr() ); } this.buildHTMLstr = function( ){ return grvGenHook( this.getWidgetID() ); } this.paintHTML = function( ) { var visible = this.visible; grvSetElemVisibility( this.getWidget(), visible ); if (visible) this.updateView(); } } Class(MVCPadScalarView,["Scalar Model"]).Extends(MVCScalarView); /** * @class This class produces a blank-padded view of the specified * {@link MVCScalarModel}. It transforms the raw value * of the model into a right-blank-padded-to-min-len format. * @extends MVCScalarView * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCPadScalarView() { /** @param {MVCScalarModel} xModel the data model to view * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, minLen, optName ) { this.souper( xModel, null, optName ); this.minLen = minLen; } /** update the display with the current model value */ this.updateView = function( ){ this.setDisplay( grvFormatPaddedStr( this.getValueStr(),this.minLen ) ); } } //////////////////////////////////////////// ////////////// Controllers ///////////////// //////////////////////////////////////////// Class(MVCController).Extends(MVCView); /** * @class This class acts as the base class for each controller * (that supports an underlying MVCView). Subclasses of MVCController * should define/override all overrides required by MVCView. * @extends MVCView * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCController() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.souper( optName ); } } Class(MVCButtonController,["descriptive text", "event handler"]) .Extends(MVCController); /** * @class This class is a Controller that<pre> * (1) watches a {@link MVCBoolModel} from which the "enable" state can * be deduced (actually only uses {@link MVCBoolModel#isTrue}), * (2) manages a button view using the enable state, * (3) defines a "pushed" event. *</pre> * @extends MVCController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCButtonController() { /** * @param {MVCBoolModel} optEnableModel optional "is enabled" data model * @param {String} evtHandlerName name of event handler function * @param {String} optName optional name of this instance * @param {String} desc tooltip description for this button */ this.konstructor = function( desc, evtHandlerName, optEnableModel, optName ) { this.souper( "BUTTON:"+optName ); // init instance variables this.altText = desc; this.eventHandler = eval( evtHandlerName );//lazy bind if (optEnableModel) this.watchModel( optEnableModel ); } /** Return whether this button should be enabled. * If no enable model was defined then we are always enabled. * @type boolean */ this.isEnabled = function(){ return this.model ? this.model.isTrue() : true; } /** return the alternate text given the current state of this view @type String */ this.getAltText = function( enabled ){ return (enabled?"":"disabled:")+ this.altText; } /** put the HTML in <a target="_blank" * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a> * given the specified enable flag and this controller's state * <p>ABSTRACT: sub classes of MVCButtonController should override this</p> */ this.canonical = function( enableFlag ) { grvMustOverride("MVCButtonController.canonical"); } this.paintHTML = function(){ this.canonical( this.isEnabled() ); } } Class(MVCFormButtonController,["button label", "descriptive text", "event handler"]) .Extends(MVCButtonController); /** * @class This class is a Button Controller that manages a form button * @extends MVCButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCFormButtonController() { /** * @param {String} label label of the button * @param {MVCBoolModel} optEnableModel optional "is enabled" data model * @param {String} evtHandlerName name of event handler function * @param {String} optName optional name of this instance * @param {String} desc tooltip description for this button */ this.konstructor = function( label, desc, evtHandlerName, optEnableModel, optName ) { this.souper( desc, evtHandlerName, optEnableModel, optName ); // init instance variables this.label = label; } /** put the HTML in <a target="_blank" * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a> * given the specified enable flag and this controller's state */ this.canonical = function( enableFlag ) { var txtBtn = this.getWidget(); if ( enableFlag ) { txtBtn.onclick = this.eventHandler; txtBtn.title = this.getAltText( enableFlag ); txtBtn.readonly = false; txtBtn.className = 'mvcFormBtn'; } else { txtBtn.onclick = null; txtBtn.title = this.getAltText( enableFlag ); txtBtn.readonly = true; txtBtn.className = 'mvcFormBtn mvcFormBtnRO'; } } this.buildHTMLstr = function(){ return "<input type='button' value='"+this.label+"' id='"+this.getWidgetID()+"'></input>"; } } Class(MVCImgButtonController,["button image filename", "descriptive text", "event handler"]) .Extends(MVCButtonController); /** * @class This class is a ButtonController that manages an image button * @extends MVCButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCImgButtonController() { /** * @param {MVCBoolModel} optEnableModel optional "is enabled" data model * @param {String} evtHandlerName name of event handler function * @param {String} optName optional name of this instance * @param {String} imgFN filename of the (enabled) button image * @param {String} desc tooltip description for this button */ this.konstructor = function( imgFN, desc, evtHandlerName, optEnableModel, optName ) { this.souper( desc, evtHandlerName, optEnableModel, optName ); // init instance variables this.imgFilename = imgFN; } /** put the HTML in <a target="_blank" * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a> * given the specified enable flag and this controller's state */ this.canonical = function( enableFlag ) { var imgBtn = this.getWidget(); if ( enableFlag ) { imgBtn.src = kMVCImgPath + this.imgFilename; imgBtn.onclick = this.eventHandler; imgBtn.title = this.getAltText( enableFlag ); imgBtn.style.cursor = "pointer"; } else { imgBtn.src = kMVCImgPath + kMVCDim_ + this.imgFilename; imgBtn.onclick = null; imgBtn.title = this.getAltText( enableFlag ); imgBtn.style.cursor = "not-allowed"; } } this.buildHTMLstr = function(){ return "<img align='middle' src='"+kMVCImgPath+this.imgFilename//kMVCImgSpacer +"' id='"+this.getWidgetID()+"'>"; } } Class(MVCCmdButtonController,["undo/redo flag"]) .Extends(MVCFormButtonController); /** * @class This class is a Button Controller for "undo/redo" buttons. * @extends MVCFormButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCCmdButtonController() { /** * @param {boolean} isUndo true if this is undo button else redo button */ this.konstructor = function( isUndo ) { this.souper( isUndo?"Undo":"Redo", "foo", isUndo?"onMVCUndoBtnPressed":"onMVCRedoBtnPressed", null, isUndo?"undo":"redo" ); this.isUndo = isUndo; } /** return the alternate text for this button * @param {String} enabled a string with the description of what * is about to be undone/redone or null if button should not be enabled * @type String */ this.getAltText = function( enabled ){ return (this.isUndo ? "[ctrl-z] UNDO: " : "[ctrl-y] REDO: ") + (enabled ? enabled : "No more to "+(this.isUndo?"undo":"redo")); } this.isEnabled = function(){ return this.isUndo ? gMVCUndoCmds.hasUnDo() : gMVCUndoCmds.hasReDo(); } } Class(MVCCmdImgButtonController,["button image filename", "undo/redo flag"]) .Extends(MVCImgButtonController); /** * @class This class is an Image Button Controller for "undo/redo" buttons. * @extends MVCImgButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCCmdImgButtonController() { /** * @param {boolean} isUndo true if this is undo button else redo button * @param {String} imgFN filename of the (enabled) button image */ this.konstructor = function( imgFN, isUndo ) { this.souper( imgFN, "foo", isUndo?"onMVCUndoBtnPressed":"onMVCRedoBtnPressed", null, (isUndo?"undo":"redo") ); this.isUndo = isUndo; } /** return the alternate text for this button * @param {String} enabled a string with the description of what * is about to be undone/redone or null if button should not be enabled * @type String */ this.getAltText = function( enabled ){ return (this.isUndo ? "[ctrl-z] UNDO: " : "[ctrl-y] REDO: ") + (enabled ? enabled : "No more to "+(this.isUndo?"undo":"redo")); } this.isEnabled = function(){ return this.isUndo ? gMVCUndoCmds.hasUnDo() : gMVCUndoCmds.hasReDo(); } } Class(MVCTxtButtonController,["button text model", "descriptive text", "event handler"]) .Extends(MVCButtonController); /** * @class This class is a Button Controller that manages a button that can * display text based on a data model's current value * @extends MVCButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCTxtButtonController() { /** * @param {MVCScalarModel} labelModel data model with button label text * @param {MVCBoolModel} optEnableModel optional "is enabled" data model * @param {String} evtHandlerName name of event handler function * @param {String} optName optional name of this instance * @param {String} desc tooltip description for this button */ this.konstructor = function( labelModel, desc, evtHandlerName, optEnableModel, optName ) { this.souper( desc, evtHandlerName, optEnableModel, optName ); // init instance variables this.labelModel = labelModel; } /** put the HTML in <a target="_blank" * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a> * given the specified enable flag and this controller's state */ this.canonical = function( enableFlag ) { var txtBtn = this.getWidget(); grvSetButtonElemText( txtBtn, this.labelModel.getValue() ); if ( enableFlag ) { txtBtn.onclick = this.eventHandler; txtBtn.title = this.getAltText( enableFlag ); txtBtn.style.cursor = "pointer"; txtBtn.disabled = false; } else { txtBtn.onclick = null; txtBtn.title = this.getAltText( enableFlag ); txtBtn.style.cursor = "not-allowed"; txtBtn.disabled = true; } } this.buildHTMLstr = function(){ return "<button class='mvcFormBtn' type='button' id='"+this.getWidgetID()+"'></button>"; } } Class(MVCScalarEditController,["Scalar Model"]).Extends(MVCController); /** * @class This abstract class manages an editor of the specified * {@link MVCScalarModel}. This controller edits a single scalar value. * @extends MVCController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCScalarEditController() { /** * @param {MVCScalarModel} sModel scalar data model to edit * @param {String} optEvtHndlrName optional name of event handler function * @param {Function} optPostEdit optional post edit value cleanup function * @param {String} optName optional name of this instance */ this.konstructor = function( sModel, optName, optEvtHndlrName, optPostEdit ) { this.souper( optName ); // init instance variables this.watchModel( sModel ); this.evtHndlrName = optEvtHndlrName ? optEvtHndlrName : "onMVCScalarEditUpdate"; this.maxchars = -1;//negative means no limit this.allowCR = false;//default is "CR means accept value" this.postEditor = optPostEdit; } /** return the current (post-edit) value of the controller widget */ this.postEdit = function() {//alert("in "+this.name+".postEdit()"); var theVal = this.getCtrlValue(); if (this.postEditor) theVal = this.postEditor(theVal); else if (theVal instanceof String) theVal = grvTrimStr( theVal );//default policy return theVal; } /** set our controller widget to the given value */ this.setCtrlValue = function(x){ this.getWidget().value = x; } /** return the current value of the controller widget */ this.getCtrlValue = function( ){ return this.getWidget().value ; } /** return a string with full invocation of our event handler @type String */ this.getEvtHandlerStr = function(){ return this.evtHndlrName+"(event,'"+this.viewID+"')"; } /** set our display to the given value */ this.setDisplay = function(x){ this.setCtrlValue(x); } /** return the current value of our data model */ this.getModelValue = function( ){ return this.model.getValue( ); } /** set our data model to the given value */ this.setModelValue = function(x){ this.model.setValue(x); } /** update our display to the current value of our data model */ this.updateView = function( ){ this.setDisplay( this.getModelValue() ); } /** force this controller to the given value */ this.forceValue = function(x){ this.setCtrlValue(x); this.setModels(x); } /** set our (and our parents if we are embedded within a * {@link MVCDualController}) data model to the given value */ this.setModels = function(x){ this.setModelValue( x ); if (this.parentView && this.parentView.setModels) this.parentView.setModels( x ); } this.paintHTML = function( ){ grvSetElemVisibility( this.getWidget(), this.visible ); if (this.visible) this.updateView(); } } Class(MVCPopupMenuController,["selection model"]).Extends(MVCScalarEditController); /** * @class This class manages a popup menu which watches * a {@link MVCListModel} (specifying the menu items) * embedded within a {@link MVCSelectionModel} that reflects * which item is/should-be currently selected. [The scalar value * we "edit" is the "select index" of the MVCSelectionModel.] * Each item in the list should implement the {@link MVCMenuItem} interface. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCPopupMenuController() { /** * @param {MVCSelectionModel} sModel selection data model to control * @param {String} optEvtHndlrName optional name of event handler function * @param {String} optName optional name of this instance * @param {String} optClassName optional CSS classname to use for formatting */ this.konstructor = function( sModel, optName, optEvtHndlrName, optClassName ){ this.souper( sModel, optName, optEvtHndlrName ); this.className = optClassName ? optClassName : "mvcPopupMenu"; } this.currentContext = function(){ return new MVCContext( this.model.model.timestamp ); } this.buildHTMLstr = function() { var L = this.model.getList(); var HTML = new Array( L.getCount()+2 ); HTML.push( '<select onchange="'+this.getEvtHandlerStr() +'" class="'+this.className +'" name="' +this.getWidgetID() +'" id="' +this.getWidgetID() +'">' ); L.iterate( function(key,ithItem){ HTML.push( '<option title="bar" value="'+key+'">' + ithItem.getDescription() +'</option>' ); } ); HTML.push( '</select>' ); return HTML.join(''); } } Class(MVCFieldEditController,["Scalar Model","min chars","max chars"]) .Extends(MVCScalarEditController); /** * @class This class manages a form field editor of the * specified {@link MVCScalarModel}. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCFieldEditController() { /** * @param {MVCScalarModel} xModel scalar data model to edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {int} max maximum number of chars to support * @param {String} optClassName optional CSS classname to use for formatting * @param {Function} optKeyFilter optional keypress filter function * @param {String} optEvtHndlrName optional name of event handler function * @param {Function} optPostEdit optional post edit value cleanup function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, min, max, optClassName, optKeyFilter, optName, optEvtHndlrName, optPostEdit ) { this.souper( xModel, optName, optEvtHndlrName, optPostEdit ); // init instance variables this.minchars = min; this.maxchars = max; this.className = optClassName ? optClassName : "mvcEntryField"; this.keyFilter = optKeyFilter; } /** Produce HTML version of a scalar editor.<p> * ala {input class='className' name="paylg1" size="min" * maxlength="max" value="123456789.01" onchange="dirty('1');" * onblur="validateDollar('paylg1')"/} */ this.buildHTMLstr = function() { var keybrdEvtHandlerStr = "onMVCFieldEditKey (event,'"+this.viewID+"')"; var focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)"; return '<input onfocus="'+focusEvtHandlerStr +'" onblur="return ' + this.getEvtHandlerStr() +'" onkeypress="return '+keybrdEvtHandlerStr +'" name="' +this.getWidgetID() +'" id="' +this.getWidgetID() +'" class="' +this.className +'" size="' +this.minchars +'" maxlength="'+this.maxchars +'"/>'; } } Class(MVCDateEditController,["Date String Scalar Model","css classname"]) .Extends(MVCScalarEditController); /** * @class This class manages a Date form field editor of the * specified {@link MVCScalarModel}. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCDateEditController() { // init "static" Class methods/elements if (grvIsUndefined(MVCDateEditController.LaunchCalendar)){//keep this line above jsdoc comments! /** Static method to launch a calendar window with given ID * @param {String} HTML ID of target form element */ MVCDateEditController.LaunchCalendar = function( fieldName ) { show_calendar( fieldName, "getElementById(self.opener.gVID)" ); } } /** * @param {MVCScalarModel} xModel scalar data model to edit * @param {String} className the CSS classname to use for formatting * @param {String} optEvtHndlrName optional name of event handler function * @param {Function} optPostEdit optional post edit value cleanup function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, className, optName, optEvtHndlrName, optPostEdit ) { this.souper( xModel, optName, optEvtHndlrName, optPostEdit ); // init instance variables this.className = className; this.keyFilter = grvDateKeyFilter; } this.calendarID = function(){ return this.getWidgetID()+".cal"; } /** TODO fix bug where being made invisible then visible again * leaves calendar icon still invisible...probably need to make * calendar its own Controller */ this.paintHTML = function( ){ grvSetElemVisibility( this.getWidget(), this.visible ); grvSetVisibility( this.calendarID(), this.visible); if (this.visible) this.updateView(); } /** Produce HTML version of a Data string scalar editor. */ this.buildHTMLstr = function() { var fieldName = this.getWidgetID(); var keybrdEvtHandlerStr = "onMVCFieldEditKey (event,'"+this.viewID+"')"; var focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)"; var calHTML = '<img alt="Launch calendar window" id="' +this.calendarID()+'" src="' +kMVCImgPath+kMVCImgCalendar +'" onclick="MVCDateEditController.LaunchCalendar(\''+fieldName+'\')" border="0"/>'; return calHTML +'<input onfocus="' + focusEvtHandlerStr +'" onblur="return ' +this.getEvtHandlerStr() +'" onkeypress="return '+ keybrdEvtHandlerStr +'" name="' +fieldName +'" id="' +fieldName +'" class="'+this.className +'" size="10" maxlength="10"/>'; } } Class(MVCTextEditController,["Scalar Model","rows","cols","max chars","css classname"]) .Extends(MVCScalarEditController); /** * @class This class manages a form field "textarea" editor of the * specified {@link MVCScalarModel}. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function MVCTextEditController() { /** * @param {MVCScalarModel} xModel scalar data model to edit * @param {int} rows number of rows to support * @param {int} cols number of cols to support * @param {int} max maximum number of chars to support * @param {String} className the CSS classname to use for formatting * @param {Function} optKeyFilter optional keypress filter function * @param {String} optEvtHndlrName optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, rows, cols, max, className, optKeyFilter, optName, optEvtHndlrName ) { this.souper( xModel, optName, optEvtHndlrName ); // init instance variables this.rows = rows; this.cols = cols; this.maxchars = max; this.allowCR = true; this.className = className; this.keyFilter = optKeyFilter; } /** Produce HTML version of a textarea editor.<p> * ala {textarea NAME="comments" cols=40 rows=6}{/textarea} */ this.buildHTMLstr = function() { var keybrdEvtHandlerStr = "onMVCFieldEditKey (event,'"+this.viewID+"')"; var focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)"; return '<textarea onfocus="'+focusEvtHandlerStr +'" onblur="return ' +this.getEvtHandlerStr() +'" onkeypress="return ' + keybrdEvtHandlerStr +'" name="' +this.getWidgetID() +'" id="' +this.getWidgetID() +'" class="'+this.className +'" rows="' +this.rows +'" cols="' +this.cols +'"/></textarea>'; } } Class(MVCChkBoxEditController,["Scalar Model","css classname"]) .Extends(MVCScalarEditController); /** * @class This class manages a form field "checkbox" editor of the * specified {@link MVCScalarModel}. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCChkBoxEditController() { /** * @param {MVCScalarModel} xModel scalar data model to edit * @param {String} className the CSS classname to use for formatting * @param {String} optEvtHndlrName optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, className, optName, optEvtHndlrName ) { this.souper( xModel, optName, optEvtHndlrName, null ); // init instance variables this.className = className; } /** set our controller widget to the given value */ this.setCtrlValue = function(x){ this.getWidget().checked = grvBoolValue(x); } /** return the current value of the controller widget */ this.getCtrlValue = function( ){ return this.getWidget().checked; } /** Produce HTML version of a checkbox editor.<p> * ala {INPUT type="checkbox" name="ACTIVE_ONLY" value="Y" readonly="readonly"/} */ this.buildHTMLstr = function() { //hack to fix safari/chrome onblur bug: send onchange events to the same handler return '<input type="checkbox"' +'" onchange="return '+this.getEvtHandlerStr() +'" onblur="return ' +this.getEvtHandlerStr() +'" name="' +this.getWidgetID() +'" id="' +this.getWidgetID() +'" class="'+this.className +'"/>'; } } Class(MVCBoolEditController,["Bool Model","css classname"]) .Extends(MVCScalarEditController); /** * @class This class manages a scalar editor with a set of yes,no,unknown * radio buttons attached to the specified Boolean (i.e. can be null) * {@link MVCBoolModel}. * @extends MVCScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCBoolEditController() { /** * @param {MVCBoolModel} xModel scalar data model to edit * @param {String} className the CSS classname to use for formatting * @param {String} optEvtHndlrName optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, className, optName, optEvtHndlrName ) { this.souper( xModel, optName, optEvtHndlrName, null ); // init instance variables this.className = className; this.visible = false;//HACK!! this.lastviz = true;//HACK!!! } //BUG IN Internet Exploder causes radio buttons to not display //on being made visible after being invisible... //SO, rebuild rather than repaint HACK!! this.mustRebuild = function() { var becameVisible = this.visible && !this.lastviz; //if (becameVisible) grvBreak("must? rebuild= "+becameVisible+" me="+itsMe+" v="+this.visible+" l="+this.lastviz); this.lastviz = this.visible; return becameVisible; } this.btnID = function(){ return this.getWidgetID() + ".rb"; } this.getButtons = function(){ return document.getElementsByName( this.btnID() ); } this.setCtrlValue = function(x) { var buttons = this.getButtons(); x = x ? x : ""; //null is represented by empty string for (var i=0; i<buttons.length; ++i) buttons[i].checked = (x==buttons[i].value); } this.getCtrlValue = function() { var buttons = this.getButtons(); for (var i=0; i<buttons.length; ++i) if (buttons[i].checked) return buttons[i].value; return null; } this.radBtnHTMLstr = function( name, evtHndlrStr, code, desc, optChecked ) { return '<input type="radio" value="'+code+'"' +'" onclick="return '+evtHndlrStr +'" NAME="'+name +'" class="'+this.className +'" STYLE="background-color:transparent"' + (optChecked ? ' checked="checked"' : '') +'/>'+desc+' <br/> '; } this.buildHTMLstr = function() { var wid = this.getWidgetID(); var name = this.btnID(); var ehs = this.getEvtHandlerStr(); return grvGenHook( wid, this.radBtnHTMLstr( name, ehs, 'Y', 'Yes' ) + this.radBtnHTMLstr( name, ehs, 'N', 'No' ) + this.radBtnHTMLstr( name, ehs, '', 'Unknown' ) ); } this.paintHTML = function() { var widget = this.getWidget(); grvSetElemVisibility( widget, this.visible ); //BUG IN Internet Exploder causes radio buttons to not display //on being made visible after being invisible...BTW, putting //an alert() here to debug it, makes the problem stop <sigh> if (this.visible) this.updateView(); var buttons = this.getButtons(); for (var i=0; i<buttons.length; ++i) grvSetElemVisibility( buttons[i], this.visible ); } } Class(MVCDollarEditController,["Scalar Model"]).Extends(MVCFieldEditController); /** * @class This class manages an editor of the specified Dollar * {@link MVCScalarModel}. * @extends MVCFieldEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDollarEditController() { /** * @param {MVCScalarModel} xModel scalar data model to edit * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, optName ){ this.souper( xModel, 15, 20, "mvcEntryfield", grvDollarKeyFilter, optName ); } this.setDisplay = function(x){ this.getWidget().value = parseFloat(x).toFixed(2); } this.getCtrlValue = function(){ var s = grvDollarStrFilter( this.getWidget().value ); if ( grvIsEmpty(s)) return 0; // if (!grvIsSignedFloat(s)) return 0; return parseFloat(s); } } Class(MVCDualController,["scalar model","css classname"]).Extends(MVCController); /** * @class This abstract class manages dual views (viewer and editor) * of a {@link MVCScalarModel}. * @extends MVCController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualController() { /** * @param {MVCScalarModel} sModel scalar data model to view/edit * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance */ this.konstructor = function( sModel, className, optName ) { this.souper( optName ); // init instance variables this.watchModel( sModel ); this.editing = false; this.viewer = null; this.editor = null; this.className = className; this.mode = MVCEditRule.kEdit; } this.enableEdit = function( enable ){ this.editing = enable; } this.formatted = function( value ){ return this.viewer.formatted(value); } this.setModels = function( value ){ this .model._setValue( value ); this.editor.model._setValue( value ); } this.setEditMode = function( mode, inEdit ) { this.mode = mode; if (mode==MVCEditRule.kZero) this.setModels( 0 ); this.enableEdit( inEdit && !MVCEditRule.IsReadOnly(mode) ); } /** validate data and return null if valid else error msg @type String */ this.validator = function() { return MVCEditRule.Validate( this.model, this.mode ); } /** update Validity attributes @return errmsg @type String */ this.updateValidity = function() { var errMsg = this.validator(); this. model._setValidity( errMsg ); this.editor.model._setValidity( errMsg ); return errMsg; } this.paintHTML = function() { this.editor.setVisible( this.visible && this.editing ); this.viewer.setVisible( this.visible && !this.editing ); var errMsg = this.updateValidity(); var widget = this.editor.getHook();//we want the wrapper HTML element! if (!widget || !widget.parentNode) return; var parent = widget.parentNode; widget = this.editor.getWidget(); if (widget==null){ grvBreak("missing editor widget"); widget = parent; }//HACK!! if (errMsg) { parent.title = widget.title = errMsg; //dont style popup menus after all... // parent.className = widget.className = this.className+'bad'; parent.className = this.className+'bad'; if (!(this.editor instanceof MVCPopupMenuController)) widget.className = parent.className; } else { parent.title = widget.title = parent.parentNode.title; //dont style popup menus after all... // parent.className = widget.className = this.className; parent.className = this.className; if (!(this.editor instanceof MVCPopupMenuController)) widget.className = parent.className; } } this.buildHTMLstr = function() { var hookID = this.getWidgetID(); var HTML = new Array(); HTML.push( this.embedHTML( hookID+".edit", this.editor ) ); HTML.push( this.embedHTML( hookID+".view", this.viewer ) ); return HTML.join(''); } } Class(MVCDualChkBoxController,["Scalar Model","min chars", "css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified (boolean) Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualChkBoxController() { /** * @param {MVCScalarModel} xModel scalar data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, min, className, optName ) { this.souper( xModel, className, optName ); // init instance variables this.viewer = new MVCPadScalarView( xModel, min ); this.editor = new MVCChkBoxEditController( xModel, className ); } } Class(MVCDualBoolController,["Bool Model","min chars", "css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified (Boolean i.e. can be null) Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualBoolController() { /** * @param {MVCBoolModel} xModel scalar data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, min, className, optName ) { this.souper( xModel, className, optName ); // init instance variables this.viewer = new MVCPadScalarView( xModel, min ); this.editor = new MVCBoolEditController( xModel, className ); } } Class(MVCDualDateController,["Date String Model", "css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified Date Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualDateController() { /** * @param {MVCScalarModel} xModel scalar data model to view/edit * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, className, optName ) { this.souper( xModel, className, optName ); // init instance variables this.viewer = new MVCScalarView( xModel ); this.editor = new MVCDateEditController( xModel, className ); } } Class(MVCDualStringController,["Scalar Model","min chars", "max chars", "css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified String Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualStringController() { /** * @param {MVCScalarModel} xModel scalar data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {int} max maximum number of chars to support * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance * @param {Function} optKeyFilter optional keypress filter function */ this.konstructor = function( xModel, min, max, className, optName, optKeyFilter ) { this.souper( xModel, className, optName ); // init instance variables this.viewer = new MVCPadScalarView( xModel, min ); this.editor = new MVCFieldEditController( xModel,min,max,className,optKeyFilter ); } } Class(MVCDualTextController,["Scalar Model","rows","cols","max chars","css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified "Text" Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualTextController() { /** * @param {MVCScalarModel} xModel scalar data model to view/edit * @param {int} rows number of rows to support * @param {int} cols number of cols to support * @param {int} max maximum number of chars to support * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, rows, cols, max, className, optName ) { this.souper( xModel, className, optName ); // init instance variables this.viewer = new MVCScalarView( xModel, grvFormatMultilineStr ); this.editor = new MVCTextEditController( xModel,rows,cols,max,className ); } } Class(MVCDualDollarController,["Scalar Model","css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and editor) of the * specified Dollar Scalar Model. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualDollarController() { /** * @param {MVCScalarModel} xModel scalar data model to view/edit * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance * @param {Function} optFormatFunction optional formatter function */ this.konstructor = function( xModel, className, optFormatFunction, optName ) { this.souper( xModel, className, optName ); // init instance variables var formatter = grvIsUndefined( optFormatFunction ) ? grvFormatDollarNot : optFormatFunction; this.viewer = new MVCScalarView( this.model, formatter ); this.editor = new MVCDollarEditController( this.model ); } this.validator = function() { if (this.mode==MVCEditRule.kZero) this.setModels( 0 );//@todo is this needed?? return MVCEditRule.Validate( this.model, this.mode ); } } Class(MVCDualMenuController,["Menu Selection Model","scalar model","css classname"]) .Extends(MVCDualController); /** * @class This class manages dual views (viewer and popup menu) of the * specified Models. * @extends MVCDualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCDualMenuController() { /** * @param {MVCSelectionModel} mModel selection data model for menu * @param {MVCScalarModel} sModel scalar data model to view * @param {String} className the CSS classname to use for formatting * @param {String} optName optional name of this instance * @param {String} optEvtHndlrName optional name of menu event handler function * @param {String} optInitDesc optional initial description string */ this.konstructor = function( mModel, sModel, className, optName, optEvtHndlrName, optInitDesc ) { this.souper( sModel, className, null, optName ); // init instance variables this.initialVal = sModel.getValue(); this.initialDesc = mModel.getDescription( sModel.getValue() ); if (optInitDesc && !mModel.loaded) this.initialDesc = optInitDesc; //logic changed to suit selection models that are not loaded yet this.viewer = new MVCScalarView( sModel, /*formatter method (of MVCScalarView)*/ function( value ) { var s = (value==this.parentView.initialVal) ? this.parentView.initialDesc : this.parentView.editor.model.getDescription(value) return (gGrvTraceTODO ? value+":" : "") + s; } ); this.editor = new MVCPopupMenuController( mModel, "menu for "+optName, optEvtHndlrName, className ); // default data validation rule this.setEditMode( MVCEditRule.kNonZ, false ); } this.setModels = function( value ){ this.model._setValue( value ); } } //////////////////////////////////////////// //////////////// Commands ////////////////// //////////////////////////////////////////// // ---------------------------------------------------------------------------- // Utility routines for making AJAX requests // ---------------------------------------------------------------------------- Class(MVCAJAXReplyCmd,["xml request object","xsl DOM"]).Extends(MVCCommand); /** * @class This class implements the "process AJAX reply" command. * NOTE: It isnt undoable (because we lose all edits!). * @extends MVCCommand * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCAJAXReplyCmd() { /** @param {grvXMLreq} xmlReq the XML request being replied to * @param {ActiveXObject} xslDOM xsl DOM doc to transform reply XML * @param {String} optName optional name of this instance */ this.konstructor = function( xmlReq, xslDOM, optName ) { this.souper( optName ); this.canUndo = false; //translate received XML into javascript (via XSL) and save try { this.script = xmlReq.xform2( xslDOM ); } catch(e) { this.state = -1; } } this.doit = function() { if (gGrvTraceEvt) grvDebugWindow( this.script ); eval( this.script ); } } Class(MVCScalarEditCmd,["view ID"]).Extends(MVCCommand); /** * @class This class implements a scalar edit command. *<p> * This code should work with either standalone * scalar edit controllers, or, with scalar edit * controllers that are embedded within "dual" * controllers that contain editor and viewer * child controllers and therefore may have data * model(s) at the "dual" level to update that are * separate from the edit controller data model. * @extends MVCCommand * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function MVCScalarEditCmd() { /** @param {String} viewID ID of controller generating this command */ this.konstructor = function( viewID ) { this.souper("edit"); this.viewID = viewID; this.editCtrl = gMVCRootView.getSubView( viewID ); if ( this.editCtrl ) { this.dual = (this.editCtrl.parentView && this.editCtrl.parentView instanceof MVCDualController) ? this.editCtrl.parentView : null; // this.newValue = this.editCtrl.getCtrlValue(); this.oldValue = this.editCtrl.getModelValue(); this.newValue = this.editCtrl.postEdit(); if (this.oldValue != this.newValue) { this.oldDesc = this.getFormattedValue( this.oldValue ); this.newDesc = this.getFormattedValue( this.newValue ); } else { this.state = -1; this.editCtrl.updateView(); } } else { this.state = -1; grvError("missing "+viewID); } } /** return the formatted version of the given value using controller's formatter */ this.getFormattedValue = function( value ) { var x = this.dual ? this.dual.formatted(value) : value; return x==" " ? 0 : x; //Extreme Hack!! } /** stuff the given value into our controller's view */ this.updateController = function(value){ this.editCtrl.forceValue( value ); } /** set our controller to the new value */ this.doit = function(){ this.updateController( this.newValue ); } /** set our controller to the old value */ this.undo = function(){ this.updateController( this.oldValue ); } this.details = function(){ return this.dual.model.name + " from '" + this.oldDesc + "' to '" + this.newDesc + "'"; } } //////////////////////////////////////////// ////////// Static Event Handlers /////////// //////////////////////////////////////////// /** undo-button-pressed event handler * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function onMVCUndoBtnPressed(){ gMVCUndoCmds.unDo(); } /** redo-button-pressed event handler * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function onMVCRedoBtnPressed(){ gMVCUndoCmds.reDo(); } /** pre-screen all keypress events for entire page * This implementation handles ctrl-z and ctrl-y * to invoke Undo and Redo respectively. * @param {event} e browser event * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function onMVCGlobalKeyPress(e) { e = e || window.event; var code = e.keyCode || e.which; if ( code==26 || code==122 && e.ctrlKey) { onMVCUndoBtnPressed(); return true; } if ( code==25 || code==121 && e.ctrlKey) { onMVCRedoBtnPressed(); return true; } } /** ScalarEditController update-event handler * @param {event} e browser event * @param {String} viewID view ID of the {@link MVCScalarEditController} * generating this event. * @return event success?? flag * @type boolean * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function onMVCScalarEditUpdate( e, viewID ) { mvcDoCmd( new MVCScalarEditCmd( viewID ) ); return true; } /** handle "key pressed" events in text fields * @param {event} e browser event * @param {String} viewID view ID of the {@link MVCFieldEditController} * generating this event. * @return false if this key should be suppressed * @type boolean * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function onMVCFieldEditKey( e, viewID ) { e = e || window.event; var code = e.keyCode || e.which; var targ = e.target || e.srcElement; if (targ.nodeType == 3) // defeat Safari bug targ = targ.parentNode; // browser handles ctrl-z ["undo"] in a text field. var SEC = gMVCRootView.getSubView( viewID ); // If "enter" key pressed, cause update event [indirectly via blur] var isEnter = (code==13 || code==3); if (isEnter && !SEC.allowCR) { targ.blur(); targ.select(); return false; } // otherwise validate value length // (and key if user specified a keyFilter). var currentValue = SEC.getWidget().value; if (SEC.maxchars>=0 && currentValue.length>SEC.maxchars) return false; // keyFilters should return the KEYCODE of the canonical new char // or null if the current char was not legal if (SEC.keyFilter) { code = SEC.keyFilter( code, currentValue ); if (code!=null) { if (e.keyCode) e.keyCode = code; else e.which = code; return true; } else return false; } return true; } /** * handle "entering a text field" event per <a target="_blank" * href="http://www.webreference.com/js/tips/000805.html">webreference tip</a>) * @param {event} e browser event * @param {Element} field HTML element of text field generating this event * @author Bruce Wallace (PolyGlotInc.com) * @version 2.5 */ function onMVCFieldEditFocus( e, field ) { // field.focus(); // field.blur(); field.select(); } //////////////////////////////////////////// //////////// Utility Functions ///////////// //////////////////////////////////////////// /** * Create and embed, as a subview, a {@link MVCScalarView} of the specified * data model attribute. * @param {MVCView} parentView the view to embed the new view into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view * @param {Function} optFormatter optional formatter function * @param {Object} optParam optional parameter to pass to model method * if the attribute actually refers to a method rather than a data element * @return the HTML string of the parentView (that includes the new embedded subview) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedAttributeViewer( parentView, attribute, optFormatter, optParam ) { var model = new MVCROAttributeModel( parentView.model, attribute, optParam ); var view = new MVCScalarView( model, optFormatter ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualDollarController} of * the specified data model attribute which is expected to be a dollar * amount data element. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {String} className the CSS classname to use for formatting * @param {Function} optFormatFunction optional formatter function * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedDollarDualEditor( parentView, attribute, className, optFormatFunction ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualDollarController( model, className, optFormatFunction ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualChkBoxController} of * the specified data model attribute which is expected to be a (boolean) * data element. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {String} className the CSS classname to use for formatting * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedChkBoxDualEditor( parentView, attribute, min, className ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualChkBoxController( model, min, className ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualBoolController} of * the specified data model attribute which is expected to be a (boolean) * data element. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {String} className the CSS classname to use for formatting * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedBoolDualEditor( parentView, attribute, min, className ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualBoolController( model, min, className ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualStringController} of * the specified data model attribute which is expected to be a String * data element. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {int} min minimum number of chars to support (i.e. display size) * @param {int} max maximum number of chars to support * @param {String} className the CSS classname to use for formatting * @param {Function} optKeyFilter optional keypress filter function * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedStringDualEditor( parentView, attribute, min, max, className, optKeyFilter ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualStringController( model, min, max, className, "string dual", optKeyFilter ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualTextController} of * the specified data model attribute which is expected to be a String * data element. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {int} rows number of rows to support * @param {int} cols number of cols to support * @param {int} max maximum number of chars to support * @param {String} className the CSS classname to use for formatting * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedTextDualEditor( parentView, attribute, rows, cols, max, className ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualTextController( model, rows, cols, max, className ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualDateController} of * the specified data model attribute which is expected to be a String * data element containing a Date. * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {String} className the CSS classname to use for formatting * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedDateDualEditor( parentView, attribute, className ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualDateController( model, className, "date dual" ); return parentView.embedAttr( attribute, view ); } /** * Create and embed, as a subview, a {@link MVCDualMenuController} of * the specified data model attribute * @param {MVCView} parentView the view to embed the new controller into * @param {String} attribute the identifier of the attribute of the * parent view's primary data model to view/edit * @param {MVCSelectionModel} menuModel data model for the popup menu * @param {String} className the CSS classname to use for formatting * @param {String} optEvtHndlrName optional edit event handler (default * is to launch a {@link MVCScalarEditCmd} command). * @param {String} optInitDesc optional initial description string * @return the HTML string of the parentView (that includes the new embedded subviews) * @type String * @author Bruce Wallace (PolyGlotInc.com) * @version 2.0 */ function mvcEmbedDualMenu( parentView, attribute, menuModel, className, optEvtHndlrName, optInitDesc ) { var model = new MVCAttributeModel( parentView.model, attribute ); var view = new MVCDualMenuController(menuModel,model,className,"menu dual",optEvtHndlrName,optInitDesc); return parentView.embedAttr( attribute, view ); } grvTraceCmp("grvMVC.js: End");
|
The Gravey Framework and RATS RIA | |||||||
| PREV NEXT | FRAMES NO FRAMES | |||||||