|
The Gravy Framework | |||||||
| 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).
Author: Bruce Wallace (PolyGlotInc.com)
Requires:
| Class Summary | |
| AttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object. |
| BoolModel | This class encapsulates a Boolean scalar data model. |
| ButtonController | This class is a Controller that(1) watches a BoolModel from which the "enable" state can be deduced (actually only uses BoolModel.isTrue()), (2) manages an image button view of the enable state, (3) defines a "pushed" event. |
| CmdButtonController | This class is a ButtonController for "undo/redo" buttons. |
| Collection | the Collection "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) ) - 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
|
| Command | This class acts as the abstract base class for each Command (ala Command design pattern). |
| Context | This class encapsulates the criteria for building a View such that if it changes, the view needs rebuilding. |
| Controller | This class acts as the base class for each controller (that supports an underlying View). |
| DequeModel | This class encapsulates a data model for Stacks and Queues. |
| DollarEditController | This class manages an editor of the specified Dollar ScalarModel. |
| DualController | This abstract class manages dual views (viewer and editor) of a ScalarModel. |
| DualDollarController | This class manages dual views (viewer and editor) of the specified Dollar Scalar Model. |
| DualMenuController | This class manages dual views (viewer and popup menu) of the specified Models. |
| EditButtonController | This class is a ButtonController for "edit" buttons which shouldnt be enabled while app is in "read only" mode. |
| FieldEditController | This class manages a form field editor of the specified ScalarModel. |
| ListModel | This class encapsulates a data model for a List of objects. |
| ListView | This class is a View that expects to subscribe to a ListModel and will invoke itemHTMLstr() on each member of the list when buildHTMLstr() is called and itemPaint() on each member when paintHTML() is called. |
| Map | This class encapsulates a Map of object/key pairs and implements the Collection virtual interface |
| MapModel | This class encapsulates a data model for a Map of object/key pairs. |
| Model | 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 Observer and Observable. |
| Mutex | This class encapsulates a Map of mutual exclusion data;
It self-registers instantiations into a static Map;
This class implements the
Wallace variation of 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. |
| Observable | the Observable "interface" has no code, only
an API followed by convention (ie DUCK-TYPING)
The required "interface" for "observables": (1) addObserver( observer ) - add given Observer to your list |
| Observer | This class acts as the abstract base class for each "observer class" (ala Observer design pattern). |
| PopupMenuController | This class manages a popup menu which watches a ListModel (specifying the menu items) embedded within a SelectionModel that reflects which item is/should-be currently selected. |
| ROAttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object. |
| ScalarEditCmd | This class implements a scalar edit command. |
| ScalarEditController | This abstract class manages an editor of the specified ScalarModel. |
| ScalarModel | This class encapsulates a Scalar data model with a default implementation of the scalar being implemented via a (bean-like) property. |
| ScalarView | This class produces a view of the specified ScalarModel. |
| SelectionModel | This class encapsulates the data model for a selector which indicates the currently selected item in a specified Collection data model. |
| UndoRedoModel | This class encapsulates the data model for the Command Dequeue which supports deep undo and redo. |
| View | This class acts as the base class for each (MVC) View. |
| Method Summary | |
static boolean
|
DoCmd( command )
Do the given command in "synchonized" mode (meaning that it will wait until commands that are already running/queued have finished). |
static void
|
DRAWVIEWS()
static function to draw the root view (and hence all views) |
static String
|
EmbedAttributeViewer( <View> parentView, <String> attribute, <Function> optFormatter, <Object> optParam )
Create and embed, as a subview, a ScalarView of the specified data model attribute. |
static String
|
EmbedDollarDualEditor( <View> parentView, <String> attribute, <String> className )
Create and embed, as a subview, a DualDollarController of the specified data model attribute which is expected to be a dollar amount data element. |
static DualMenuController
|
EmbedDualMenu( <View> parentView, <String> attribute, <SelectionModel> menuModel, <String> className, <Function> optEvtHndlr )
Create and embed, as a subview, a DualMenuController of the specified data model attribute |
static void
|
MUTEX_CPU_SLICE( <int> cmdID, <int> optStartID )
static routine to give a slice of CPU to mutex with given ID |
static void
|
OnFieldEditFocus( <Element> field )
handle "entering a text field" event per webreference tip) |
static boolean
|
OnFieldEditKey( <String> viewID )
handle "key pressed" events in text fields |
static Object
|
OnGlobalKeyPress()
pre-screen all keypress events for entire page This implementation handles ctrl-z and ctrl-y to invoke Undo and Redo respectively. |
static void
|
OnRedoBtnPressed()
redo-button-pressed event handler |
static boolean
|
OnScalarEditUpdate( <String> viewID )
ScalarEditController update-event handler |
static void
|
OnUndoBtnPressed()
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 mvc.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). * @author Bruce Wallace (PolyGlotInc.com) * @requires utils.js * @version 1.0 */ /** * @class the Collection "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) ) - 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 *</pre> * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Collection(){/* this function exists only for JsDoc purposes.*/} Class(Map); /** * @class This class encapsulates a Map of object/key pairs * and implements the {@link Collection} virtual interface * @extends OObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Map() { /** @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 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 } /** 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(); } } Class(Context); /** * @class This class encapsulates the criteria for building * a {@link View} 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 OObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Context() { /** @param {anyComparableType} optContext optional value to save as view context */ this.konstructor = function( optContext ) { // init instance variables this.context = optContext; } /** * @param {Context} 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 Observable "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 Observer} to your list *</pre> * @see Model */ function Observable(){/* this function exists only for JsDoc purposes.*/} Class(Observer); /** * @class This class acts as the abstract base class for each * "observer class" (ala Observer design pattern). *<pre> * Subclasses of Observer 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 OObject * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Observer() { 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. * @memberx Observer * @param {Observable} 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 * @memberx Observer * @param {Observable} observable object to monitor */ this.subscribe = function( observable ) { this.model = observable; observable.addObserver( this ); } } // ----------------------- // --- COMMAND PATTERN --- // ----------------------- Class(Command); /** * @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 Command 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 OObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Command() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { // init static member variables if (!Command.NextID) Command.NextID = 1; // init instance variables if (optName) this.name = optName; this.state = 0; //-1=invalid,0=notDone,1=done,2=undone,3=redone this.canUndo = true; this.id = Command.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 this.DOIT() */ this.syncDoIt = function(){ (new Mutex(this,"DOIT")); } /** synchronized this.UNDO() */ this.syncUnDo = function(){ (new Mutex(this,"UNDO")); } /** synchronized this.REDO() */ this.syncReDo = function(){ (new Mutex(this,"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."; gRootView.block(); this.doit(); this.state = 1; gRootView.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."; gRootView.block(); this.redo(); this.state = 3; gRootView.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: gRootView.block(); this.undo(); this.state = 2; gRootView.unblock(); break; default: throw "Cant UNDO a command that isnt done."; } } } Class(Mutex,["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 the <a target="_blank" * href="http://www.polyglotinc.com/Mutex/"> * Wallace variation of 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 OObject * @see #konstructor * @see GLOBALS#MUTEX_CPU_SLICE * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Mutex() { /** @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 static member variables if (!Mutex.Map) Mutex.Map = new Map("global mutex map"); // 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" Mutex.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 = timestamp(); this.choosing = false; MUTEX_CPU_SLICE( 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 GLOBALS#MUTEX_CPU_SLICE */ this.cpuSlice = function( optStartID ) { var startID = optStartID ? optStartID : Mutex.Map.firstKey(); for (var j=Mutex.Map.getItem(startID); j; j=Mutex.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) ) ) ){ BusyDo( "MUTEX_CPU_SLICE", '('+ 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. Mutex.Map.delItem( this.id ); } } /** static routine 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. * @see Mutex */ function MUTEX_CPU_SLICE( cmdID, optStartID ) { // Break("slice id="+cmdID+" start="+optStartID); Mutex.Map.getItem(cmdID).cpuSlice(optStartID); } //////////////////////////////////////////// ///////////////// MODELS /////////////////// //////////////////////////////////////////// Class(Model).Extends(Observer); /** * @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 Observer} and {@link Observable}. * This class implements the {@link Observable} interface. * @extends Observer * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Model() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.Observer(); //super() // init instance variables if (optName) this.name = optName; this.hasChanged = false; //only since last notifyObservers this.everChanged = false; //since last "neverChanged()" this.subscribers = new Array(); this.autoCommit = true; 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 + "; "); Break(s); } /** return "this" formatted as string @type String */ this.toString = function(){ return ObjectToShortInitializer(this); } /** set the "dirty" and "everChanged" flags to true */ this.dirty = function(){ this.hasChanged = this.everChanged = true; } /** clear the "dirty" flag that says "there has been a change to * this model since the last update event broadcase via publish()" */ this.clean = function(){ this.hasChanged = false; } /** return current value of funky "everChanged" flag */ this.neverChanged = function(){ this.everChanged = false; } /** tell observers that we have changed * @param {boolean} resetMark if true clear funky "everChanged" flag * @param {Object} optAdhocObj optional adhoc object to pass to observers */ this.publish = function( resetMark, optAdhocObj ) { this.dirty(); if (resetMark) this.neverChanged(); if (this.autoCommit) 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(false,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() { if (!this.autoCommit) Error("already started transaction"); this.autoCommit = false; } /** publish a "batch" of updates * @param {boolean} totalReload clears "everChanged" flag iff true */ this.EndTransaction = function(totalReload) { if (this.autoCommit) Error("Not in transaction!"); this.autoCommit = true; if (totalReload) this.neverChanged(); this.updateStamp(); this.notifyObservers(); } /** {@link Observable} API */ this.addObserver = function( observer ) { ValidateArgs(["observer"]); this.subscribers.push( observer ); } /** @deprecated @throws not implemented exception */ this.delObserver = function( observer ) { ValidateArgs(["observer"]); throw "Not Implemented Yet"; //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) { gRootView.block(); var N = this.subscribers.length; for (var i=0; i<N; ++i) { //TraceMVC(this.name+"["+optAdhocObj+"] updates["+i+"] "+this.subscribers[i].name); this.subscribers[i].update( this, optAdhocObj ); } gRootView.unblock(); } this.clean(); } } Class(ListModel).Extends(Model); /** * @class This class encapsulates a data model for a List of objects. * This class implements the {@link Collection} interface. * @extends Model * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ListModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.Model( optName ); //super() // init instance variables this._reset(); } /** debug method to return this list as a string @type String */ this.dump = function() { var N = this.list.length; var s = this.name + "===>"; for (var i=0; i<N; ++i) s += (this.list[i].dump() + "; "); return s; } /** 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 item @type Object */ this._push = function(o){ this.list.push(o); return o; } /** clear list and update timestamp but dont publish */ this._reset = function( ){ this.list = new Array(); this.updateStamp(); } /** 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 item @type Object */ this.addItem = function(o){ this._push(o); this.publish(); return o; } /** clear list and publish */ this.reset = function( ){ this._reset(); this.publish(true); } /** 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(); } } Class(DequeModel).Extends(ListModel); /** * @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 ListModel * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function DequeModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ){ this.ListModel( optName ); //super() } /** 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(MapModel).Extends(Model); /** * @class This class encapsulates a data model for a Map of * object/key pairs. Its API is a wrapper for the {@link Map} API. * This class implements the {@link Collection} interface. * @extends Model * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function MapModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.Model( optName ); //super() // init instance variables this.map = new Map("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(true); } this.iterate = function(f) { this.map.iterate(f); } } /** The suffix added to the basic name to get the validity attribute name. */ var kValidityAttributeSuffix = ".err"; Class(ScalarModel).Extends(Model); /** * @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 Model * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ScalarModel() { /** @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.Model( optName ); //super() // 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+kValidityAttributeSuffix, errMsg ); } /** return the validity attribute of this scalar value @type String */ this.getValidity = function(){ return this. GET( this.pname+kValidityAttributeSuffix ); } } Class(BoolModel).Extends(ScalarModel); /** * @class This class encapsulates a Boolean scalar data model. * @extends ScalarModel * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function BoolModel() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ){ this.ScalarModel( "boolflag", optName ); //super() } /** 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(AttributeModel,["base object","attribute"]).Extends(ScalarModel); /** * @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 ScalarModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function AttributeModel() { /** @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.ScalarModel( undefined, optName?optName:attribute ); //super() // 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+kValidityAttributeSuffix ] = errMsg; } /** return the validity attribute of this attribute @type String */ this.getValidity = function(){ return this.base[ this.attribute+kValidityAttributeSuffix ]; } } Class(ROAttributeModel,["base object","attribute"]).Extends(AttributeModel); /** * @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 AttributeModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ROAttributeModel() { /** @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.AttributeModel( baseObject, attribute, optName ); //super() // 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(){ Error("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; } } var kPropertyIdSelect = 'selected'; var kNothingSelected = -1; Class(SelectionModel,["Collection Model"]).Extends(ScalarModel); /** * @class This class encapsulates the data model for a selector * which indicates the currently selected item in a specified * Collection 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 ScalarModel * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function SelectionModel() { /** @param {Object} collModel Collection data model we select from * @param {String} optName optional name of this instance */ this.konstructor = function( collModel, optName ) { this.ScalarModel( kPropertyIdSelect, optName ); //super() // init instance variables this._select( kNothingSelected ); 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 Collection model */ this.getCount = function( ){ return this.getList().getCount(); } /** return the currently selected Collection item */ this.getSelection = function( ){ return this.getItem( this.getValue() ); } /** return the currently selected Collection item formatted as string @type String */ this.getSelectionStr = function( ){ return this.getSelection().toString(); } /** return the currently selected Collection item formatted as description @type String */ this.getDescription = function(i){ return this.getItem(i).getDescription(); } /** select the specified Collection item but dont publish * @param {Object} k key of item to select */ this._select = function(k){ this._setValue(k); } /** select the specified Collection item and publish * @param {Object} k key of item to select */ this.select = function(k){ this. setValue(k); } /** 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(true);} /** re-select the current value and publish */ this.reselect = function( ){ this.publish(); } /** handle Collection model update event by "unselect"ing */ this.update = function( ){ if ( this.getList().hasChanged ) this.unselect(); // TraceEvt("UnSelectUpdate["+this+"]"); } /** return the index into the Collection model of the current selection * @type int */ this.getIndex = function( ) { var index = 0; var goal = this.getValue(); this.model.iterate( function(key,o,i){ if (key==goal){ index = i; return true; } } ); return index; } } Class(UndoRedoModel).Extends(DequeModel); /** * @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 Command API. * @extends DequeModel * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function UndoRedoModel() { this.konstructor = function(){ this.DequeModel( "undo/redo commands" ); //super() } /** 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 */ var gUndoCmds = new UndoRedoModel(); /** 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 */ function DoCmd( command ) { if ( command.isInvalid() ) return false; if ( command.canUndo ) gUndoCmds.newDo( command ); else command.syncDoIt(); return true; } //////////////////////////////////////////// ///////////////// VIEWS /////////////////// //////////////////////////////////////////// Class(View); /** * @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 Controller} event causes some {@link Model} * 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 a Context subclass object * containing the driver information for building this view. *</pre> * @extends OObject * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function View() { /** @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 Map( this.name+" subviews" ); this.parentView = null; this.embeddedSubViews = false; //if true subviews are built via buildHTMLstr() this.lastBuildContext = null; this.setViewID( optViewID ); //init static member variables if (View.Depth==undefined) View.Depth = 0; } /** 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 Context */ this.currentContext = function(){ return new Context( 0 ); /*override me*/ } /** register the given model as our primary data model * @param {Model} 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 View subclass can say what the "inner" ID * is when there is a wrapper. * @type String */ this.getWidgetID = function() { var noWrapper = this.parentView && this.parentView.embeddedSubViews; return( noWrapper ? this.viewID : this.innerID() ); } /** 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 = getHook( 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 = getHook( 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 Context} for this view */ this.updateContext = function(){ this.lastBuildContext = this.currentContext(); } /** return true iff the view context has changed @type boolean */ this.contextChanged = function(){ var same = this.currentContext().sameAs( this.lastBuildContext ); return ! same; } /** 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) { setVisibility( 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; //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) TraceMVC(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; var rebuild = optForceRebuild || this.rebuildAny(); //TraceEvt("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; 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 View*/ 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 View */ 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(); } // NOTE: DONT! call busy() 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(){ ++View.Depth; } /** API to block/unblock view updating (to stop redraw thrashing) */ this.unblock = function(){ if (--View.Depth == 0) DRAWVIEWS(); } } /** Global Root Container View (initialized to empty container) */ var gRootView = new View( "root view", "fauxRootHook", true ); /** static function to draw the root view (and hence all views) */ function DRAWVIEWS(){ gRootView.draw(); } Class(ListView).Extends(View); /** * @class This class is a View that expects to subscribe to a * {@link ListModel} 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 ListView should define/override: * (A) {@link #itemHTMLstr} which creates HTML for specified item * (B) {@link #itemPaint} which decorates HTML for specified item *</pre> * @extends View * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ListView() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.View( optName ); //super() // init instance variables //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 ListModel * @param {Object} item the actual item from our ListModel * @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 ListModel * @param {Object} item the actual item from our ListModel * @param {String} itemID the view ID of the corresponding item subview */ this.itemPaint = function(index,item,itemID){ /* abstract */ } /** return the view ID for the item subview specified * @param {int} index index into our ListModel/ListView * @type String */ this.itemViewID = function(index){ return this.getWidgetID() + index; } /** invoke {@link #itemPaint} for each item in our list */ this.paintHTML = function() { 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(); var listView = this; this.model.iterate( function( i, ithItem ){ HTML[i] = 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(ScalarView,["Scalar Model"]).Extends(View); /** * @class This class produces a view of the specified * {@link ScalarModel}. 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 View * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ScalarView() { /** @param {ScalarModel} 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.View( optName ); //super() // 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 genHook( this.getWidgetID() ); } this.paintHTML = function( ) { var visible = this.visible; setElemVisibility( this.getWidget(), visible ); if (visible) this.updateView(); } } /** * Create and embed, as a subview, a {@link ScalarView} of the specified * data model attribute. * @param {View} 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 */ function EmbedAttributeViewer( parentView, attribute, optFormatter, optParam ) { var viewID = parentView.getWidgetID() + "." + attribute; var model = new ROAttributeModel( parentView.model, attribute, optParam ); return parentView.embedHTML( viewID, new ScalarView( model, optFormatter ) ); } //////////////////////////////////////////// ////////////// Controllers ///////////////// //////////////////////////////////////////// Class(Controller).Extends(View); /** * @class This class acts as the base class for each controller * (that supports an underlying View). Subclasses of Controller * should define/override all overrides required by View. * @extends View * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function Controller() { /** @param {String} optName optional name of this instance */ this.konstructor = function( optName ) { this.View( optName ); //super() } } Class(ButtonController,["button image filename", "descriptive text", "event handler"]) .Extends(Controller); /** * @class This class is a Controller that<pre> * (1) watches a {@link BoolModel} from which the "enable" state can * be deduced (actually only uses {@link BoolModel#isTrue}), * (2) manages an image button view of the enable state, * (3) defines a "pushed" event. *</pre> * @extends Controller * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ButtonController() { /** * @param {BoolModel} 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.Controller( "BUTTON:"+optName ); //super() // init instance variables this.imgFilename = imgFN; 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 */ this.canonical = function( enableFlag ) { var imgBtn = this.getWidget(); if ( enableFlag ) { imgBtn.src = kImgPath + this.imgFilename; imgBtn.onclick = this.eventHandler; imgBtn.alt = this.getAltText( enableFlag ); imgBtn.style.cursor = "hand"; } else { imgBtn.src = kImgPath + kDim_ + this.imgFilename; imgBtn.onclick = null; imgBtn.alt = this.getAltText( enableFlag ); imgBtn.style.cursor = "not-allowed"; } } this.buildHTMLstr = function(){ return "<img align='middle' src='"+kImgPath+kImageSpacer +"' name='"+this.getWidgetID()+"'>"; } this.paintHTML = function(){ this.canonical( this.isEnabled() ); } } /** undo-button-pressed event handler */ function OnUndoBtnPressed(){ gUndoCmds.unDo(); } /** redo-button-pressed event handler */ function OnRedoBtnPressed(){ gUndoCmds.reDo(); } /** pre-screen all keypress events for entire page * This implementation handles ctrl-z and ctrl-y * to invoke Undo and Redo respectively. */ function OnGlobalKeyPress() { if ( event.keyCode==26 ) { OnUndoBtnPressed(); return true; } if ( event.keyCode==25 ) { OnRedoBtnPressed(); return true; } } Class(CmdButtonController,["button image filename", "undo/redo flag"]) .Extends(ButtonController); /** * @class This class is a ButtonController for "undo/redo" buttons. * @extends ButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function CmdButtonController() { /** * @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.ButtonController( imgFN, "foo", isUndo?"OnUndoBtnPressed":"OnRedoBtnPressed", null, (isUndo?"undo":"redo") ); //super() 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 ? gUndoCmds.hasUnDo() : gUndoCmds.hasReDo(); } } Class(EditButtonController,["button image filename", "descriptive text", "event handler","enable model"]) .Extends(ButtonController); /** * @class This class is a ButtonController for "edit" buttons * which shouldnt be enabled while app is in "read only" mode. * @extends ButtonController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function EditButtonController() { /** * @param {BoolModel} enableModel "is enabled" data model * @param {String} evtHandler 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, evtHandler, enableModel, optName ){ this.ButtonController ( imgFN, desc, evtHandler, enableModel, optName ); //super() } this.paintHTML = function(){ this.canonical( kReadOnly ? false : this.isEnabled() ); } } Class(ScalarEditCmd,["view ID"]).Extends(Command); /** * @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 Command * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ScalarEditCmd() { /** @param {String} viewID ID of controller generating this command */ this.konstructor = function( viewID ) { this.Command("edit"); //super() this.viewID = viewID; this.editCtrl = gRootView.getSubView( viewID ); if ( this.editCtrl ) { this.dual = (this.editCtrl.parentView && this.editCtrl.parentView instanceof DualController) ? this.editCtrl.parentView : null; this.newValue = this.editCtrl.getCtrlValue(); this.oldValue = this.editCtrl.getModelValue(); 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; Error("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; } } /** ScalarEditController update-event handler * @param {String} viewID view ID of the {@link ScalarEditController} * generating this event. * @return event success?? flag * @type boolean */ function OnScalarEditUpdate( viewID ) { DoCmd( new ScalarEditCmd( viewID ) ); return true; } Class(ScalarEditController,["Scalar Model"]).Extends(Controller); /** * @class This abstract class manages an editor of the specified * {@link ScalarModel}. This controller edits a single scalar value. * @extends Controller * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function ScalarEditController() { /** * @param {ScalarModel} sModel scalar data model to edit * @param {String} optEvtHndlr optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( sModel, optName, optEvtHndlr ) { this.Controller( optName ); //super() // init instance variables this.watchModel( sModel ); this.evtHndlrName = optEvtHndlr ? optEvtHndlr : "OnScalarEditUpdate"; } /** 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.getEvtHandler = function( ){ return this.evtHndlrName+"('"+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 DualController}) 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( ){ setElemVisibility( this.getWidget(), this.visible ); if (this.visible) this.updateView(); } } Class(PopupMenuController,["selection model"]).Extends(ScalarEditController); /** * @class This class manages a popup menu which watches * a {@link ListModel} (specifying the menu items) * embedded within a {@link SelectionModel} that reflects * which item is/should-be currently selected. [The scalar value * we "edit" is the "select index" of the SelectionModel.] * @extends ScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function PopupMenuController() { /** * @param {SelectionModel} sModel selection data model to control * @param {String} optEvtHndlr optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( sModel, optName, optEvtHndlr ){ this.ScalarEditController( sModel, optName, optEvtHndlr ); //super() } this.buildHTMLstr = function() { var L = this.model.getList(); var HTML = new Array( L.getCount()+2 ); HTML.push( '<select title="foo" onchange="'+this.getEvtHandler() +'" class="entryfield" name="'+this.getWidgetID()+'">' ); L.iterate( function(key,ithItem){ HTML.push( '<option title="bar" value="'+key+'">' + ithItem.getDescription() +'</option>' ); } ); HTML.push( '</select>' ); return HTML.join(''); } } /** handle "key pressed" events in text fields * @param {String} viewID view ID of the {@link FieldEditController} * generating this event. * @return false if this key should be suppressed * @type boolean */ function OnFieldEditKey( viewID ) { // browser handles ctrl-z ["undo"] in a text field. // If "enter" key pressed, cause update event [indirectly via blur] var char = event.keyCode; var isEnter = (char==13 || char==3); if (isEnter) { event.srcElement.blur(); event.srcElement.select(); return false; } // otherwise validate key if user specified a keyFilter. var SEC = gRootView.getSubView( viewID ); if (SEC.keyFilter) { return SEC.keyFilter( char, SEC.getWidget().value ); } 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 {Element} field HTML element of text field generating this event */ function OnFieldEditFocus( field ) { // field.focus(); // field.blur(); field.select(); } Class(FieldEditController,["Scalar Model"]).Extends(ScalarEditController); /** * @class This class manages a form field editor of the * specified {@link ScalarModel}. * @extends ScalarEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function FieldEditController() { /** * @param {ScalarModel} xModel scalar data model to edit * @param {Function} optKeyFilter optional keypress filter function * @param {String} optEvtHndlr optional name of event handler function * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, optKeyFilter, optName, optEvtHndlr ) { this.ScalarEditController( xModel, optName, optEvtHndlr ); //super() // init instance variables this.keyFilter = optKeyFilter; } /** Produce HTML version of a scalar editor.<p> * ala {input class='entryfield' name="paylg1" size="15" * value="123456789.01" onchange="dirty('1');" * onblur="validateDollar('paylg1')"/} */ this.buildHTMLstr = function( ) { var keybrdEvent = "OnFieldEditKey ('"+this.viewID+"')"; var focusEvent = "OnFieldEditFocus(this)"; return '<input onfocus="'+focusEvent +'" onblur="return '+this.getEvtHandler() +'" onkeypress="return '+keybrdEvent +'" NAME="'+this.getWidgetID() +'" class="entryfield" size="15" maxlength="20"/>'; } } Class(DollarEditController,["Scalar Model"]).Extends(FieldEditController); /** * @class This class manages an editor of the specified Dollar * {@link ScalarModel}. * @extends FieldEditController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function DollarEditController() { /** * @param {ScalarModel} xModel scalar data model to edit * @param {String} optName optional name of this instance */ this.konstructor = function( xModel, optName ){ this.FieldEditController( xModel, dollarKeyFilter, optName ); //super() } this.setDisplay = function(x){ this.getWidget().value = parseFloat(x).toFixed(2); } this.getCtrlValue = function(){ var s = dollarStrFilter( this.getWidget().value ); if ( isEmpty(s)) return 0; // if (!isSignedFloat(s)) return 0; return parseFloat(s); } } //////////////////////////////////////////////////////////////////////////////// // Constants defining Edit Rules for DualControllers var kEditRulePos = '+'; //positive or zero var kEditRuleNeg = '-'; //negative or zero var kEditRuleNonZ = '#'; //non-zero var kEditRuleZero = '0'; //R/O zero var kEditRuleCopy = 'X'; //R/O copy of balance A var kEditRuleRO = 'R'; //R/O (current value) var kEditRuleEdit = '?'; //editable - no validation Class(DualController,["scalar model","css classname"]).Extends(Controller); /** * @class This abstract class manages dual views (viewer and editor) * of a {@link ScalarModel}. * @extends Controller * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function DualController() { /** * @param {ScalarModel} 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.Controller( optName ); //super() // init instance variables this.watchModel( sModel ); this.editing = false; this.viewer = null; this.editor = null; this.className = className; this.mode = kEditRuleEdit; } /** validate data and return null if valid else error msg @type String */ this.validator = function(){ return null; /*override me*/ } 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 ) { switch( this.mode = mode ) { case kEditRuleZero: this.setModels( 0 ); //fall thru... case kEditRuleCopy: case kEditRuleRO: this.enableEdit( false ); break; default: this.enableEdit( inEdit ); } } /** 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){ Break("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 PopupMenuController)) 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 PopupMenuController)) 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(DualDollarController,["Scalar Model","css classname"]) .Extends(DualController); /** * @class This class manages dual views (viewer and editor) of the * specified Dollar Scalar Model. * @extends DualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function DualDollarController() { /** * @param {ScalarModel} 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.DualController( xModel, className, optName ); //super() // init instance variables var formatter = (optFormatFunction===undefined) ? format_dollar_not : optFormatFunction; this.viewer = new ScalarView( this.model, formatter ); this.editor = new DollarEditController( this.model ); } /** Validate data based on STD edit rules:<pre> * '0' Read-Only; zero * 'X' Read-Only; copy of balance A * 'R' Read-Only; (current value) * '+' Read-Write; positive or zero required * '-' Read-Write; negative or zero required * '#' Read-Write; non-zero required * '?' Read-Write; no validation *</pre> * @return null if valid else error msg * @type String */ this.validator = function() { var value = this.model.getValue(); switch( this.mode ) { case kEditRulePos: if (value>=0) break; return "Value ["+value+"] must be Positive."; case kEditRuleNeg: if (value<=0) break; return "Value ["+value+"] must be Negative."; case kEditRuleNonZ: if (value!=0) break; return "Value ["+value+"] must be Non-Zero."; case kEditRuleZero: this.setModels( 0 ); break; default: break; } return null; } } Class(DualMenuController,["Menu Selection Model","scalar model","css classname"]) .Extends(DualController); /** * @class This class manages dual views (viewer and popup menu) of the * specified Models. * @extends DualController * @see #konstructor * @author Bruce Wallace (PolyGlotInc.com) * @version 1.0 */ function DualMenuController() { /** * @param {SelectionModel} mModel selection data model for menu * @param {ScalarModel} 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} optEvtHndlr optional name of menu event handler function */ this.konstructor = function( mModel, sModel, className, optName, optEvtHndlr ) { this.DualController( sModel, className, null, optName ); //super() // init instance variables this.viewer = new ScalarView( sModel, /*formatter method (of ScalarView)*/ function( value ) { return this.parentView.editor.model.getDescription(value); } ); this.editor = new PopupMenuController( mModel, "menu for "+optName, optEvtHndlr ); // default data validation rule this.setEditMode( kEditRuleNonZ, false ); } this.setModels = function( value ){ this.model._setValue( value ); } /** Validate data based on subset of STD edit rules:<pre> * 'R' Read-Only; (current value) * '#' Read-Write; non-zero required * '?' Read-Write; no validation *</pre> * @return null if valid else error msg * @type String */ this.validator = function() { switch( this.mode ) { case kEditRuleNonZ: if (this.model.getValue()==0) return "Must select a known value."; default: break; } return null; } } /** * Create and embed, as a subview, a {@link DualDollarController} of * the specified data model attribute which is expected to be a dollar * amount data element. * @param {View} 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 */ function EmbedDollarDualEditor( parentView, attribute, className ) { var viewID = parentView.getWidgetID() + "." + attribute; var model = new AttributeModel( parentView.model, attribute ); var view = new DualDollarController( model, className ); parentView[ attribute ] = view; //squirrel away reference to view return parentView.embedHTML( viewID, view ); } /** * Create and embed, as a subview, a {@link DualMenuController} of * the specified data model attribute * @param {View} 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 {SelectionModel} menuModel data model for the popup menu * @param {String} className the CSS classname to use for formatting * @param {Function} optEvtHndlr optional edit event handler (default * is to launch a {@link ScalarEditCmd} command). * @return the newly created viewer/editor * @type DualMenuController */ function EmbedDualMenu( parentView, attribute, menuModel, className, optEvtHndlr ) { var dataModel = new AttributeModel( parentView.model, attribute ); var viewID = parentView.menuID(); return parentView.embedView( viewID, new DualMenuController( menuModel,dataModel,className,viewID,optEvtHndlr) ); }
|
The Gravy Framework | |||||||
| PREV NEXT | FRAMES NO FRAMES | |||||||