/* -*- javascript -*-
     Copyright 2006 TJM Enterprises, Inc..
     All Rights Reserved
System        : BROWSER_BASE_JS :
     Object Name   : $RCS_FILE$
     Revision      : $REVISION$
     Date          : Thu May 11 16:08:43 2006
     Created By    : Bradford McKesson, TJM Enterprises, Inc.
     Created       : Thu May 11 16:08:43 2006

     Last Modified : <030310.0724>
     ID            : $Id: js.etf,v 2.5 2004/03/25 21:58:00 jon Exp $
     Source        : $Source: /export/cvs/cvsroot/me/macros/js.etf,v $
     Description
     Notes
     $Log: js.etf,v $
     Revision 2.5  2004/03/25 21:58:00  jon
     Final release from steve

 * Browser-side dispatcher and standard widgets.
 * 2009-06-29 bwm: patch FF3 Element.dispatchEvent() new behavior.
    dispatchEvent() works in FF3, fails silently in FF2.
 * 2009-04-02 bwm: patch "dispatcher is not a constructor" JS error on some non-entry pages
 * 2008-10-05 bwm: created w.bbtool.t_money.round()
   to help ensure consistency in invoice total calcs vs
   report total calcs as far as displayed totals go.
 * 2008-07-18 bwm: bbrecord.receive_insert() and
 * 2008-02-16 bwm: Array.prototype.index_of()
 * 2007-11-28 BWM patch alias
*/
var ns4 = document.layers;
var ie4 = document.all;
var nn6 = document.getElementById && !document.all;

window.bb = new Object();

// constructor
// dispatcher/registry
window.bb.dispatcher = function() {
    this.__FILE__ = 'browser_base.js';

    this.event_type = new Object();
    this.event_list = new Object();

    this.event_type.js_event = 'js_event';
    this.event_type.synth_event = 'synthetic_event';

    // known js events
    this.event_list.click = this.event_type.js_event;
    this.event_list.change = this.event_type.js_event;
    this.event_list.blur = this.event_type.js_event;
    this.event_list.focus = this.event_type.js_event;
    this.event_list.mouseover = this.event_type.js_event;
    this.event_list.mouseout = this.event_type.js_event;

    this.event_list.keydown = this.event_type.js_event;
    this.event_list.keypress = this.event_type.js_event;
    this.event_list.keyup = this.event_type.js_event;
    this.event_list.keystroke = this.event_type.synth_event;

    // keyboard navigation keypress; handle arrow keys,
    // pageup/down home, end
    // this.event_list.nav_keypress = this.event_type.synth_event;

    // look up our data in window.pmap
    this.get_model = function() {
        this.error("call OBSELETE get_model()");
        return this.local_window.pmap[this.table];
    };


    // synthetic event support
    // cause a change event
    this.do_change = function (inElement) {
        this._send_event('change', inElement);
    }

    // cause a blur event
    this.do_blur = function (inElement) {
        this._send_event('blur', inElement);
    }

    // Private
    // use one of the do_ functions
    // programmatically trigger a normal JS event.
    this._send_event = function (inEventName, inElement) {
        // General idea: create event object,
        // send it to inElement
        /*
         from http://www.howtocreate.co.uk/tutorials/javascript/domevents
Events
    Covers all event types.
HTMLEvents
    Covers 'abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select', 'submit', and 'unload'.
UIEevents
    Covers 'DOMActivate', 'DOMFocusIn', 'DOMFocusOut', and (since they do not have their own key events module in DOM 2) it also covers 'keydown', 'keypress', and 'keyup'. Also indirectly covers MouseEvents.
MouseEvents
    Covers 'click', 'mousedown', 'mousemove', 'mouseout', 'mouseover', and 'mouseup'.
MutationEvents
    Covers 'DOMAttrModified', 'DOMNodeInserted', 'DOMNodeRemoved', 'DOMCharacterDataModified', 'DOMNodeInsertedIntoDocument', 'DOMNodeRemovedFromDocument', and 'DOMSubtreeModified'.
*/
        var elem = to_element(inElement);

        var ne = null;
        if (document.createEvent && elem.dispatchEvent) {
            // W3C DOM level 2
            switch(inEventName) {
            case 'change', 'blur', 'focus':
                ne = document.createEvent("HTMLEvents");
                ne.initEvent(inEventName, true, true);
            case 'click', 'mouseover', 'mouseout':
            default :
                ne = document.createEvent("MouseEvents");
                // ### make mouse coords be inside inElement box
                ne.initMouseEvent(inEventName, true, true, window,
                                        0, 0, 0, 0, 0, false, false, false, false, 0, null);
            }
            var ffv = geckoGetRv();
            if (ffv < 1.090000) {
                this.info('Direct exec (' + inEventName + ',' + elem.name + ')');
                this.list_dispatch.apply(elem, [ne]);
            } else {
                // w3c
                // Only works in FF3+
                this.info('send_event(' + inEventName + ',' + elem.name + ')');
                elem.dispatchEvent(ne);
            }
        } else {
            this.warn('Attempting non-W3C event dispatch');
            ne = new Object();
            ne.target = elem;
            ne.type = inEventName;
            var command = 'on'.inEventName;
            elem[command](ne);
        }

        // call list_dispatch()
    }

    this.registered_events = new Object();
    this.current_wait_id = 1;

    // Private
    // generate 1 or more event invokation identifiers
    // return as array()
    this.next_wait_ids = function(N) {
        if ( (N == null) || (N < 1) ) { N = 1; }
        var result = new Array();
        for (var i = 0; i < N; i++) {
            result[i] =  this.current_wait_id + ':';
            this.current_wait_id++;
            if (this.current_wait_id > 999999) {
                this.current_wait_id = 0;
            }
        }
        return result;
    }


    // Protected
    // The standard event handler.
    // Work around event model differences IE vs W3C vs bugs.
    // Allow multiple handlers per element/event hook.
    // x-browser issues taken from http://www.quirksmode.org/dom/w3c_events.html
    this.list_dispatch = function(evt) {

        try {
        var e = evt?evt:window.event;
        var b = 'event_list_' + e.type + '_';
        window.dispatcher.info('list_dispatch '+b);
        var t = null;
        if (e.target) {
            // W3C
            t = e.target;
        } else if (e.srcElement) {
            // IE
            t = e.srcElement;
        }
        if (t.nodeType == 3) {
            // Bug old FF, Safari
            // got text node instead of element
            t = t.parentNode();
        }

        var out = true;
        var recv = this; // element receiving the event
        if (recv[b]) {
            var ic = recv[b].length;
            var i = 0;
            for(i = 0; i < ic; i++) {
                if (recv[b][i]) {
                    if (1 && recv[b][i].deferred_invoke) {
                        window.dispatcher.serialized.add(recv[b][i], e);
                        //out = false;
                    } else {
                        out = recv[b][i].invoke(e);
                    }
                }
            }
        } else {
            // this.list_dispatch should not have been called
            window.debug('invalid event list ' + t.tagName + ', '+b.toString() + ' this.tagName: ' + recv.tagName);
        }
        } catch (ex) {
            window.error('list_dispatch: event failed '+ex.toString());
            // the event is finished
        }
        return out;
    };

    // Protected
    // Event handler constructor for client side events
    // The 'this' for event handlers is the inDOMObject
    this.handler = function(inEvent, inDOMObject, inHandler, inArgs, inLabel) {
        if (!inLabel) {
            window.dispatcher.error('No label for handle: '+inEvent + ', '+inDOMObject.tagName );
        }
        this.func = inHandler;
        this.arglist = inArgs;
        this.type = inEvent; // expected event type
        this.target = inDOMObject; // expected target-- browsers get this wrong
        this.label = inLabel;

        this.has_func = function(inFunc) {
            return this.func == inFunc;
        }

        // call the handler in context of inDOMObject.
        // inEventObject is the browser event object.
        // deferred invoke loses event
        this.invoke = function(inEventObj) {
            window.dispatcher.debug("calling handler for "+(inEventObj?inEventObj.type:'') + " on " + this.target.tagName);
            this.func.apply(this.target, [this.target, this.arglist, inEventObj]);
        }
    };

    // the browser event object (inEventObj) self destructs
    // after a few seconds so we copy the 'important' members
    // BWM: 2007-3-16: The event object self-destructs when the next
    // JS-handled event is generated.  Collisions over the event object
    // yield 'permission denied...' exceptions
    // use: var x = new window.dispatcher.clone_event(..)
    this.clone_event = function(inEventObj) {
        this.type = inEventObj.type;
        this.target = inEventObj.target;
        this.currentTarget = inEventObj.currentTarget ? inEventObj.currentTarget : undefined;
        this.keyCode = inEventObj.keyCode;
        this.charCode = inEventObj.charCode;
        this.shiftKey = inEventObj.shiftKey;
        this.ctrlKey = inEventObj.ctrlKey;
        this.altKey = inEventObj.altKey;
        this.metaKey = inEventObj.metaKey;
        this.which = inEventObj.which;
    }

    // Protected
    // Event handler constructor for client side events
    // only one sequential_handler is ever active at a time
    this.sequential_handler = function(inEvent, inDOMObject, inHandler, inArgs, inLabel) {
        dispatcher.handler.apply(this, [inEvent, inDOMObject, inHandler, inArgs, inLabel]); // extend handler

        // wrap the invocation in a queue object
        // use: var x = new handler.deferred_invoke(event);
        this.deferred_invoke = function(inHandler, inEventObj) {
            this.wait_id = window.dispatcher.next_wait_ids()[0] +':'+ inHandler.label;
            window.dispatcher.debug("deferred "+this.wait_id+" invoke on "+(inEventObj?inEventObj.target.tagName:''));

            this.finished = false;
            this.busy = false;

            // the browser event object (inEventObj) self destructs
            // after a few seconds so we copy the 'important' members
            this.event_obj = new window.dispatcher.clone_event(inEventObj);
            this.result = null;
            this.handler = inHandler;
            this.invoke = function() {
                try {
                    this.busy = true;
                    this.result = this.handler.invoke(this.event_obj, this);
                } catch(ex) {
                    window.error('invoke: '+this.wait_id+' failed: '+ex.toString());
                    // the event is finished
                }
                this.finished = true;
                this.busy = false;
            }
        }
    }

    // queue of event handlers that are sequentialized
    // only one of these run at a time.
    this.serial_queue = function () {
        this._queue = new Array();
        this.do_serialized = function() {
            while (( this._queue.length > 0) && (this._queue[0].finished))  {
                this._queue.shift();
            }
            if (this._queue.length <= 0) {
                // nothing to do
                return;
            }
            var n = Date.now();
            if (this._queue[0].start_time && (n - this._queue[0].start_time > 30000)) {
                // timeout
                var late = this._queue.shift();
                window.dispatcher.error('timeout error on '+late.wait_id);
            }

            if ((this._queue.length > 0) && (!this._queue[0].busy)) {
                this._queue[0].start_time = Date.now();
                this._queue[0].invoke();
                this.do_serialized();
            }
        }

        // put inHandler on serial_queue
        this.add = function(inHandler, inEvent) {
            var x = new inHandler.deferred_invoke(inHandler, inEvent);

            this._queue.push(x);
            this.do_serialized();
        }

        // process events about once per second.
        this.scan = function() {
            window.dispatcher.serialized.do_serialized();
            setTimeout(window.dispatcher.serialized.scan, 1000);
        }
    };

    // startup event processing
    this.serialized = new this.serial_queue();
    setTimeout(this.serialized.scan, 1000);

    // Protected
    // constructor for server-event handler + server-response
    // handler.
    this.handler_aj = function(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel) {
        dispatcher.handler.apply(this, [inEvent, inDOMObject, inHandler, inArgs, inLabel]);
        this.response = inResponseHandler;

        // call the handler in context of inDOMObject.
        // inEventObject is the browser event object.
        this.invoke = function(inEventObj) {
            window.debug("calling handler_aj for "+this.type + " on " + this.target.tagName);
            this.func.apply(this.target, [this.target, this.arglist, this.response, inEventObj]);
        }
    }

    // Protected
    // constructor for server-event handler + server-response
    // handler.
    this.sequential_handler_aj = function(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel) {
        dispatcher.sequential_handler.apply(this, [inEvent, inDOMObject, inHandler, inArgs, inLabel]);
        this._response = inResponseHandler;
        this.response = undefined;

        // wrap the invocation in a queue object
        // use: var x = new handler.deferred_invoke(event);
        this.deferred_invoke = function(inHandler, inEventObj) {
            this.wait_id = window.dispatcher.next_wait_ids()[0] +':'+ inHandler.label;
            window.dispatcher.debug("deferred "+this.wait_id+" invoke on " + (inEventObj?inEventObj.target.tagName:''));

            this.finished = false;
            this.busy = false;
            this.event_obj = new window.dispatcher.clone_event(inEventObj);
            this.result = null;
            this.handler = inHandler;
            this.invoke = function() {
                this.busy = true;
                try {
                this.result = this.handler.invoke(this.event_obj, this);
                } catch(ex) {
                    window.error('invoke: '+this.wait_id+' failed: '+ex.toString());
                    // the event is finished
                    this.finished = true;
                    this.busy = false;
                }
            }

            var real_this = this; // closure ref so response knows which flags to set
            this.response = function (arg1, arg2, arg3, arg4) {
                try {
                    if (real_this.handler._response.invoke) {
                        window.dispatcher.debug("sequential_handler_aj: invoke("+to_string(arguments)+") ");
                        // real_this.result = real_this.handler._response.invoke(arguments);
                        real_this.result = real_this.handler._response.invoke.apply(real_this.handler._response, arguments);
                    } else {
                        real_this.result = real_this.handler._response.apply(this, arguments);
                    }
                } catch (e) {
                    window.error("Response "+this.wait_id+" failed: "+e.toString());
                }
                real_this.finished = true;
                real_this.busy = false;
            }
        }

        // call the handler in context of inDOMObject.
        // inEventObject is the browser event object.
        this.invoke = function(inEventObj, inDefer) {
            window.debug("calling handler_aj for "+this.type + " on " + this.target.tagName);
            this.func.apply(this.target, [this.target, this.arglist, inDefer?inDefer.response:this.response, inEventObj]);
        }
    }

    // API
    // install a local event handler/observer
    // inEvent is a DOM event name without the 'on' prefix
    this.register = function(inEvent, inDOMObject, inHandler, inArgs, inLabel) {
        var thunk = new this.sequential_handler(inEvent, inDOMObject, inHandler, inArgs, inLabel);
        return this.p_register(inEvent, inDOMObject, inHandler, inArgs, thunk);
    }

    // API
    // register a server-event function + server-response hanndler
    // inEvent is a DOM event name without the 'on' prefix
    // inHandler is expected to send an AJAX request to some server
    // inResponseHandler called when the server responds
    this.register_aj = function(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel) {
        var thunk = new this.sequential_handler_aj(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel);
        return this.p_register(inEvent, inDOMObject, inHandler, inArgs, thunk);
    }

    // API
    // call the function inHandler as though it were an event handler-- queue it up
    this.dispatch = function(inEvent, inDOMObject, inHandler, inArgs, inLabel) {
        var thunk = new this.sequential_handler(inEvent, inDOMObject, inHandler, inArgs, inLabel);
        var event_obj = new Object();
        event_obj.type = 'synthetic';
        event_obj.target = inDOMObject;
        event_obj.currentTarget = inDOMObject;
        this.serialized.add(thunk, event_obj);
    } // dispatch;

    // API
    // call the function inHandler as though it were an event handler-- queue it up
    this.dispatch_aj = function(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel) {
        var thunk = new this.sequential_handler_aj(inEvent, inDOMObject, inHandler, inResponseHandler, inArgs, inLabel);
        var event_obj = new Object();
        event_obj.type = 'synthetic_aj';
        event_obj.target = inDOMObject;
        event_obj.currentTarget = inDOMObject;
        this.serialized.add(thunk, event_obj);
    } // dispatch_aj;

    // Private
    // General method of associating an event handler to its event
    // and object.  Supports non-DOM events, multiple handlers on same event,
    // return true on success
    this.p_register = function(inEvent, inDOMObject, inHandler, inArgs, inThunk) {
        /* In the simple case, inEvent is a javascript event
         * such as 'click' or 'blur' or 'change'
         * to be addEventListener() on inDOMObject and passed inArgs
         * when triggered.
         * For synthetic events, we must arrange to call
         * them ourselves.
         */
        if (!this.event_list[inEvent]) {
            // event not defined
            this.error("Undefined event '"+inEvent+"'");
            return false;
        }
        if (!inDOMObject instanceof Element) {
            // events only work on DOM elements and windows
            this.error("Illegal object type '"+typeof(inDOMObject)+"'");
            return false;
        }
        if (!is_object(inArgs)) {
            this.error("inArgs must be an object, not '"+typeof(inArgs)+"'");
            return false;
        }

        switch(this.event_list[inEvent]) {
        case this.event_type.js_event:
            var evthandler = null;
            var already_reg = 'event_list_' + inEvent + '_';

            if (!inDOMObject[already_reg]) {
                // prepare inDOMObject to support list of event handlers.
                inDOMObject[already_reg] = new Array();
                var auto_call = 'invoke_'+inEvent;
                inDOMObject[auto_call] = this.list_dispatch;

                if (inDOMObject.addEventListener){
                    // w3c
                    inDOMObject.addEventListener(inEvent, this.list_dispatch, false);
                } else if (inDOMObject.attachEvent){
                    // ie
                    inDOMObject.attachEvent('on'+inEvent, this.list_dispatch);
                }  else {
                    this.error('cannot add '+inEvent+' handler to '+inDOMObject.nodeName);
                }

            }

            // Remove any previous registration of inHandler
            //
            // W3C DOM event API supposedly does not allow a function to be
            // registered twice for the same event, but we are using
            // anonymous functions for event handlers, which are always
            // 'different' according to JS.
            // Also, W3C DOM API does not support examining the list of
            // registered events and IE does not support W3C DOM event
            // registration.
            // Thus we maintain our own list of event handlers that this.list_dispatch
            // processes
            var found = null;
            var h = 0;
            var hl = inDOMObject[already_reg].length;
            for (h = 0; h < hl; h++) {
                if (inDOMObject[already_reg][h].has_func(inHandler)) {
                    found = h;
                    window.warn("double registration detected: "+ h);
                }
            }
            if (found != null) {
                window.warn("removing from '"+already_reg+"': "+ found);
                inDOMObject[already_reg].splice(found, 1);
            }

            inDOMObject[already_reg].push(inThunk);
            break;

        case this.event_type.synth_event:
            this.info("Synthetic events not supported yet");
            break;
        default:
            this.debug("Unknown event type for '"+inEvent+"'");
        }
        return true;
    } // dispatcher.p_register

    // Remove an event handler
    // return true on success
    this.unregister = function(inEvent, inDOMObject, inHandler) {
        if (!this.event_list[inEvent]) {
            // event not defined
            this.error("Undefined event '"+inEvent+"'");
            return false;
        }
        if (!inDOMObject instanceof Element) {
            // events only work on DOM elements and windows
            this.error("Illegal object type '"+typeof(inDOMObject)+"'");
            return false;
        }

        switch(this.event_list[inEvent]) {
        case this.event_type.js_event:
            var evthandler = null;
            var already_reg = 'event_list_' + inEvent + '_';

            if (!inDOMObject[already_reg]) {
                // nothing to do
                this.debug('event not registered');
                return true;
            }
            // actually remove the event
            var found = null;
            var h = 0;
            var hl = inDOMObject[already_reg].length;
            for (h = 0; h < hl; h++) {
                if (inDOMObject[already_reg][h].has_func(inHandler)) {
                    found = h;
                    // window.warn("double registration detected: "+ h);
                }
            }
            if (found != null) {
                window.info("removing from '"+already_reg+"': "+ found);
                inDOMObject[already_reg].splice(found, 1);
            } else {
                window.info("Handler not found in '"+already_reg+"'");
            }
            break;

        case this.event_type.synth_event:
            this.info("Synthetic events not supported yet");
            break;
        default:
            this.debug("Unknown event type for '"+inEvent+"'");
        }
        return true;
    } // dispatcher.unregister

    // Manage mapping between records and bbrecord instance that manages the
    // record. Keyed by prefix_name of record like the global
    // recordmap object.
    //
    // After post_widgetize(), there should exist one entry in
    // window.dispatcher._bbrecordmap for each entry in window.recordmap
    this._bbrecordmap = new Object();

    // API
    this.add_widget = function(inRecordInst, inPrefixName) {
        if (this._bbrecordmap[inPrefixName]) {
            // At most one bbrecord per prefix allowed
            this.error('Prefix "'+inPrefixName + '" already in map');
        }
        this._bbrecordmap[inPrefixName] = inRecordInst;
    }
    // API
    this.get_widget = function(inPrefixName) {
        if (this._bbrecordmap[inPrefixName]) {
            return this._bbrecordmap[inPrefixName];
        } else {
            this.error('Attempt to lookup invalid prefix name: "'+inPrefixName+'"');
            return undefined;
        }
    }
    // API
    this.remove_widget = function(inPrefixName) {
        if (this._bbrecordmap[inPrefixName]) {
            delete this._bbrecordmap[inPrefixName];
        } else {
            this.warn('Attempt to delete invalid prefix name: "'+inPrefixName+'"');
        }
    }

    // API
    this.get_recordmap = function(inPrefix) {
        var d = null;
        var prefix = inPrefix;
        if (prefix && (prefix != '')) {
            d = window.recordmap[prefix];
        } else {
            // top level record
            // Must match view/template_fns.php::print_record_model() usage
            d = window.recordmap['_HEAD_'];
        }
        return d;
    }
    bbtool.debug_mixin.apply(this, []);

    bbtool.observable_mixin.apply(this, []);

} // dispatcher



// base of all bb system objects
bb.bbobject = function() {

    // Like the perl isa operator.
    // _list_isa tracks class/interface types that an instance has
    // JS instanceof seems unreliable with my use of function.apply
    // for specializing classes
    this._list_isa = '|bbobject|';
    this.add_isa = function(inClassName) {
        this._list_isa += '|' + inClassName + '|';
    }
    this.isa = function(inClassName) {
        return (this._list_isa.indexOf('|' + inClassName + '|') > -1);
    }
}


// The bbwidget system is intended to be a light weight
// set of controls that provide us a more uniform way to
// to access data and handle events on various HTML chunks.

// We let the DOM and CSS do most of the work.
// This javascript only registers event handlers
// and perform CSS class switching.

// Most DHTML effects are ultimately realized as a change in
// the style of some element (visibility, position, size, color).
// We exploit the Cascade in CSS to selectively switch on/off
// various effects classes instead of working directly with elem.style...

// The other effects are by creating and/or destroying DOM elements.
// This is done elsewhere.

// Base class for all bbwidget controllers.
bb.bbwidget = function(inWidgetElem, inModelID) {
    bb.bbobject.apply(this);
    this.add_isa('bbwidget');

    // The widget container DOM element.  All html inside this element is
    // considered part of the widget
    this.elem = inWidgetElem;
    if (this.elem.widget) {
        // this element already has a widget
        window.error("DOM Element already widgetized: <" + inWidgetElem.tagName + ' id=' + inWidgetElem.Id + ' name=' + inWidgetElem.name + ' class=' + inWidgetElem.className + '>');
        delete this.elem.widget;
    }
    this.elem.widget = this;
    this.widget = this; // make event handlers work outside of a real event

    if (inModelID && window.pmap && window.pmap[inModelID]) {
        // data from the server associated with this widget.
        this.data = window.pmap[inModelID];
    } else {
        this.data = null;
    }

    this.is_active = true;

    if (window.dispatcher) {
        this.dispatcher = window.dispatcher;
    } else if (top.dispatcher) {
        this.dispatcher = top.dispatcher;
    } else {
        // fail
        alert('Program error: Dispatcher not found in widget');
    }

    this.debug = this.dispatcher.debug;
    this.error = this.dispatcher.error;
    this.info = this.dispatcher.info;
    this.warn = this.dispatcher.warn;

    // this.debug('bbwidget('+inWidgetElem.tagName + ', ' + inModelID + ')');

    this.sreplace = sreplace;

    // find and remove any occurrences of inOldClass and inNewClass
    // in this.elem.className.  Append inNewClass to this.elem.className
    this.replace_class = function(inOldClass, inNewClass) {
        replace_class(this.elem, inOldClass, inNewClass);
    }

    // disable all events on this widget
    // ### incomplete
    this.deactivate = function() {
        this.replace_class('bbactive', 'bbinactive');
        this.is_active = false;
    }

    // enable events on this widget
    // ### incomplete
    this.activate = function() {
        this.replace_class('bbinactive', 'bbactive');
        this.is_active = true;
    }

    this.show = function() {
        this.replace_class('bbinvisible', 'bbvisible');
    }

    this.hide = function() {
        this.replace_class('bbvisible', 'bbinvisible');
    }

    // called for each bbwidget instance after all bbwidgets are constructed.
    // allow initialization of things dependent on other bbwidgets.
    this.post_widgetize = function () {
    }



    // ### enclosing widgets? contained widgets?
    // We should not need to know about containment, the DOM
    // already knows the hierarchy and its event bubbling
    // should handle any widget hierarchy issues.
    // Widgets that care about containment can handle it
    // themselves.
} // bbwidget

// JS controller portion of the bbwindow widget.
bb.bbwindow = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbwindow');

    var li = getElementsByClassName(inWidgetElem, 'div', 'content');
    if (li.length) {
        this.content = li[0];
    } else {
        this.content = inWidgetElem;
    }

    // drag is combination of mousedown somewhere in widget
    // followed by series of mouseover
    this.moveto = function(x, y) {
        var unit = (typeof(this.elem.style.top) == 'number')?0:'px';
        //this.debug("bbwindow.moveto(" + x + unit + "," + y + unit + ")");
        this.elem.style.top = x + unit;
        this.elem.style.left = y + unit;
    }
    this.moveby = function(xoff, yoff) {
        var unit = (typeof(this.elem.style.top) == 'number')?0:'px';
        this.elem.style.top = this.elem.style.top + xoff + unit;
        this.elem.style.left = this.elem.style.left + yoff + unit;
    }

    // center the bbwindow within the current browser window
    this.moveto_center = function() {
        var h = window.innerHeight;
        var w = window.innerWidth;

        var maxh = h - 30;
        var maxw = w - 30;

        var plh = this.elem.offsetHeight;
        var plw = this.elem.offsetWidth;

        var unit = (typeof(this.elem.style.height) == 'number')?0:'px';
        if (plh > maxh) {
            this.elem.style.height= maxh + unit;
            plh = maxh;
        }
        if (plw > maxw) {
            this.elem.style.width = maxw + unit;
            plw = maxw;
        }

        var hscroll = window.pageYOffset;
        var wscroll = window.pageXOffset;
        this.info("moveto_center '(" + h + " - " + plh + ")/2 + " + hscroll + "'");
        var ctop = (h - plh)/2 + hscroll;
        var cleft = (w - plw)/2 + wscroll;

        return this.moveto(ctop, cleft);
    }

    bbtool.observable_mixin.apply(this, []);
    // this.append_content
} // bbwindow

// a group of records organized in a spreadsheet-like editable grid.
bb.bbdatagrid = function(inWidgetElem, inModelID) {
    bb.bbwindow.apply(this, [inWidgetElem, inModelID]); // extends bbwindow
    this.add_isa('bbdatagrid');
    // always active

    this.arrow = new bbtool.arrow_key_manager(this.elem,
                                          {row_first:3,
                                              row_current:3,
                                              do_ui_initialize:false});
    this.arrow.register();

} // bbdatagrid

// standard semi-modal dialog
// base of dialog window classes
// It should not be necessary to actually override any methods here,
// rather, use observers.
// API
// my_dialog_elem =  bbtool.dialog.new_dialog(dlgparam);
// my_dialog_elem.widget.set_prefix(this.prefix)
// Observe pre_load_html_toolbar post_load_html_toolbar
// Observe pre_connect_to_toolbar post_connect_to_toolbar
// Observe pre_show, post_show
// Observe pre_hide, post_hide
// Observe pre_load_json_content pre_format_load_json_content post_load_json_content
// Observe pre_connect_to_content post_connect_to_content
bb.bbdhtml_dialog = function(inWidgetElem, inModelID) {
    bb.bbwindow.apply(this, [inWidgetElem, inModelID]); // extends bbwindow
    this.add_isa('bbdhtml_dialog');
    this.suppress_events = 0;

    var li = getElementsByClassName(inWidgetElem, 'div', 'tool_bar');
    if (li.length) {
        this.toolbar = li[0];
    } else {
        this.toolbar = inWidgetElem;
    }

    this.get_dialog_id = function() {
        var dialog_id = this.elem.getAttribute('id');
        if (!dialog_id) {
            dialog_id = null;
        }
        return dialog_id;
    }

    // change record that the dialog operates on
    this.set_prefix = function(inPrefix) {
        if (!inPrefix) {
            this.prefix = '_HEAD_';
        } else {
            this.prefix = inPrefix;
        }
    }

    // retrieve record identifier
    // allows dispatcher.get_widget(this.get_prefix()) to obtain the
    // bbrecord being modified.
    this.get_prefix = function() {
        return this.prefix;
    }

    // Private
    // wrap apply_feature allow suppressing event handling.
    this._apply_feature = function(inPrefix, inEvent, inArgs) {
        if (this.suppress_events) {
            return this.event_proceed;
        } else {
            return this.apply_feature(inPrefix, inEvent, inArgs);
        }
    }

    // make dialog visible
    // if inSuppressEvents == true then do NOT call observer functions
    this.bbwindow_show = this.show;
    this.show = function(inSuppressEvents) {
        /*
         * pre_show
         * post_show
         */
        if (!this.prefix ) {
            window.dispatcher.error('bbdhtml_dialog.show: no prefix for dialog '+this.get_dialog_id());
            return null;
        }
        var evt = this._apply_feature(this.prefix, 'pre_show', this);
        if (evt != this.event_proceed) {
            return evt;
        }
        var ret = this.bbwindow_show(true);

        // must do bbwindow_show() first to setup offsetWidth/offsetHeight
        // computation in moveto_center()
        // but this causes flicker
        this.moveto_center();
        this._apply_feature(this.prefix, 'post_show', this);

        return ret;
    } // bbdhtml_dialog.show

    this.bbwindow_hide = this.hide;
    this.hide = function() {
        if (!this.prefix ) {
            window.dispatcher.error('bbdhtml_dialog.show: no prefix for dialog '+this.get_dialog_id());
            return false;
        }
        var evt = this._apply_feature(this.prefix, 'pre_hide', this);
        if (evt != this.event_proceed) {
            return evt;
        }
        this.bbwindow_hide();
        this._apply_feature(this.prefix, 'post_hide', this);

        return true;
    }  // bbdhtml_dialog.hide

    // delete all info in content section of dialog.
    this.clear_content = function() {
        while (this.content.firstChild) {
            this.content.removeChild(this.content.firstChild);
        }
        return true;
    } // bbdhtml_dialog.clear_content

    //
    // placeholder-- construct HTML for any buttons in the pre/post load_html_toolbar
    this.load_html_toolbar = function(inHTML) {
        var ok = true;
        var evt = this.event_proceed;
        if (ok) {
            evt = this._apply_feature(this.prefix, 'pre_load_html_toolbar', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }

        // event handler gets first shot at this.toolbar.innerHTML
        if (inHTML) {
            var th = this.toolbar.innerHTML;
            this.toolbar.innerHTML = th + inHTML;
        }

        if (ok) {
            evt = this._apply_feature(this.prefix, 'post_load_html_toolbar', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }
        return evt;
    } // load_html_toolbar

    // parse inResponseText as JSON
    // if any record[], append it to this.content
    this.load_json_content = function(inResponseText) {
        /*
         * pre_load
         * <parse json>
         * pre_format  <chance to modify the json in this.rs>
         * <convert to DOM>
         * post_load
         */
        var evt = this._apply_feature(this.prefix, 'pre_load_json_content', this);
        if (evt != this.event_proceed) {
            this.error("Event Failed");
            return evt;
        }
        var otxt = JSON.parse(inResponseText);
        var ok = (otxt instanceof Object);
        if (ok) {
            this.debug("bbdhtml_dialog otxt.receive: '"+otxt.receive+"'");
            if (otxt['receive'] == this.get_dialog_id()) {
                this.rs = otxt;
                /*
                 *  'this.rs.records' => result set of data
                 *  'this.rs.labels' => dictionary of data to display
                 *  'this.rs.use_table' => dictionary name of this result set
                 *  'this.rs.context' => disposition of result set.
                 *  'this.rs.format_param => bbtool.result_table options.
                 */
                if (!this.rs.format_param) {
                    this.rs.format_param = new Object();
                }
            } else {
                ok = false;
                this.error('Incorrect dialog id received: '+otxt['receive']+
                           ' expecting '+this.get_dialog_id());
            }
        } else if (inResponseText == '') {
            this.warn("Empty JSON response: "+inResponseText);

        } else {
            this.error("Cannot parse JSON response "+inResponseText);
        }
        if (ok) {
            // chance to modify this.rs before display
            // (especially this.rs.labels which controls the
            // appearance of the data)
            var evt = this._apply_feature
                      (this.prefix, 'pre_format_load_json_content', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }

        if (ok) {
            var temp_content = this.content.innerHTML;
            // if (this.rs.records.length > 0) {
                var rt = new bbtool.result_table
                          (this.rs.records, this.rs._columns,
                           this.rs.format_param);
                temp_content += rt.get_html();
            // } else {
            //     temp_content += "<p>----</p>";
            // }
            this.content.innerHTML = temp_content;
        }

        if (ok) {
            var evt = this._apply_feature
                      (this.prefix, 'post_load_json_content', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }
        return evt;
    } // load_json_content

    // append inResponseText to this.content
    this.load_html_content = function(inResponseText) {
        // INCOMPLETE use load_json_content
    } // load_html_content

    // widgetize the toolbar section and
    // assign any additional event handlers to the widgets
    this.connect_to_toolbar = function() {
        /*
         * connect_pre_widgetize
         * connect_post_widgetize
         * connect_pre_assign
         * connect_post_assign
         */
        var evt = this._apply_feature(this.prefix, 'pre_connect_to_toolbar', this);
        if (evt != this.event_proceed) {
            return evt;
        }
        widgetize(this.toolbar);

        evt = this._apply_feature(this.prefix, 'post_connect_to_toolbar', this);
        if (evt != this.event_proceed) {
            return evt;
        }
        return this.event_proceed;
    } // connect_to_toolbar

    // widgetize the content section and
    // assign any additional event handlers to the widgets
    this.connect_to_content = function() {
        /*
         * connect_pre_widgetize
         * connect_post_widgetize
         * connect_pre_assign
         * connect_post_assign
         */
        var evt = this._apply_feature(this.prefix, 'pre_connect_to_content', this);
        if (evt != this.event_proceed) {
            return evt;
        }

        // dialog content cannot be widgetized because it is not in recordmap
        // widgetize(this.content);

        evt = this._apply_feature(this.prefix, 'post_connect_to_content', this);
        if (evt != this.event_proceed) {
            return evt;
        }
        return this.event_proceed;
    } // connect_to_content


} //bbdhtml_dialog

// Pick from list dialog window.
// Base class of popup dialog windows.
// This class expects to present a dialog-like window to the
// user and support some simple data entry.
bb.bbchoose_dialog = function(inWidgetElem, inModelID) {
    bb.bbdhtml_dialog.apply(this, [inWidgetElem, inModelID]); // extends bbwindow
    this.add_isa('bbchoose_dialog');

    this.prefix = this.event_default_prefix;

    // only one choose_dialog may be open at a time.
    window.choose_dialog_active = false;

    // clear the content area and hide window.
    this.erase = function() {
        this.clear_content();
        window.choose_dialog_active = false;
        this.hide();
    } // bbchoose_dialog.erase

    // part of load_result. chance for
    // subclass to alter query_result, query_column before
    // display processing
    this.load_result_pre_display = function(inData) { }

    // copy inData into the dialog
    this.load_result = function(inData) {
        /* inData is object with members
         *  'records' => result set of data
         *  'labels' => dictionary of data to display
         *  'use_table' => dictionary name of this result set
         *  'context' => disposition of result set.
         */

        /*
         * whats missing:
         * ability to modify the data before/during/after display.
         */
        // stash query result as member of object that requested it.
        var w = this.widget?this.widget:this;
        w.query_result = inData.records;
        w.query_column = inData._columns;
        var total_available = w.query_result.length;

        // subclass add extra query_column, query_result data
        var ok = w.load_result_pre_display(inData);

        var temp_content = '';
        if (total_available > 0) {
            var rs = new bbtool.result_table(w.query_result, w.query_column);
            temp_content += rs.get_html();
            // rs.add_line(...)
            // ### subclass add extra rows.
        } else {
            temp_content += "<p>None Available</p>";
            // ### subclass add extra rows-- empty.
        }
        w.content.innerHTML = temp_content;
        window.choose_dialog_active = true;
    } // load_result

    // load choose_dialog with inPicks. When user clicks an item,
    // inRtnElement will receive the picked item.
    // inPicks is object with members
    this.load_picks = function(inPicks, inRtnElement) {
        return this.load_result(inPicks);
    } // bbchoose_dialog.load_picks

    // support load_picks
    // Exists solely to form closure on inValue so it is not
    // shared among all onclick event handlers of different inNodes
    this.set_onclick = function(inNode, inElem, inValue) {
        var w = this;
        var v = inValue;

        var handle = function(inElement, inA, inEv) {
            w.save_user_selection(inA.elem, inA.newvalue, this);
            w.erase();
        }

        // inElem.widget.record.queue_update(inElem, inElem.widget.data, inEv);
        window.dispatcher.register('click', inNode, handle, {elem:inElem, newvalue:inValue}, 'Find Item Change item number1');

    } // bbchoose_dialog.set_onclick

} // bbchoose_dialog



bb.bbfield = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbfield');

    // ### take over getValue, setValue
    this.dirty=false;
    this.name = this.elem.name;


    // Given name of a form element, return the part prefix.
    // The prefix is empty string for head record input elements
    this.extract_prefix = function(part)  {
        var last_open_bracket = part.lastIndexOf('[');

        var prefix_name = '';
        if (last_open_bracket > -1) {
            prefix_name = part.substring(0, last_open_bracket);
        }
        return prefix_name;
    } // bbfield.extract_prefix

    // Find, return last index in a string like part[idx][idx2]
    // The alias_name = whole field name if no []s in part
    this.extract_alias_name = function (part) {
        var last_open_bracket = part.lastIndexOf('[');
        var last_close_bracket = part.lastIndexOf(']');

        var alias_name = part;
        if (last_open_bracket > -1) {
            alias_name = part.substring(last_open_bracket + 1, last_close_bracket);
        }
        return alias_name;
    } // bbfield.extract_alias_name

    // convert the 'alias_name' to the database field name
    this.field_name_for_alias_name = function (alias, table) {
        var t = (is_string(table)&&table)?table:'';
        var do_substr = (alias.indexOf(table) >= 0);
        var out = '';
        if (do_substr) {
            out = alias.substring(t.length);
        } else {
            out = alias;
        }
        return out;
    } // bbfield.field_name_for_alias_name

    // Private
    // lookup our record's data in window.recordmap
    this._get_recordmap = function() {
        return window.dispatcher.get_recordmap(this.prefix);
    } // bbfield._get_recordmap

    this.get_record = function() {
        return this.record;
    }

    this.get_queue = function() {
        return this.record.queue;
    }

    // lookup our record in recordmap, copy it into this.data
    this.connect_to_data = function() {
        this.alias_name = this.extract_alias_name(this.elem.name);
        this.prefix = this.extract_prefix(this.elem.name);
        this.data = clone(this._get_recordmap());

        // for(var d in this.data) {
        //    info('data.'+d+' = ' +this.data[d]);
        // }
        this.data.old_value = this.elem.getValue();
        this.data.field_name = this.field_name_for_alias_name(this.alias_name, this.data.table);
        this.data.part_prefix = this.prefix;
    } // connect_to_data

    // delegate to elem
    this.getValue = function() {
        return this.elem.getValue();
    }
    this.setValue = function(inValue) {
        this.elem.setValue(inValue);
    }

    this.post_widgetize = function () {

    }


    // main
    if (this.elem.getValue) {
        // rationalized form element
        // register with the record
        this.connect_to_data();

        this.record = getAncestorByClass(this.elem, 'bbrecord');
        if (this.record) {
            if (!this.record.field_list) {
                this.record.field_list = new Array();
            }
            this.record.field_list.push(this);
        } else {
            error('No bbrecord enclosing field '+this.elem.name);
        }

        // fixup file upload controls
        var elem_type = this.elem.getAttribute('type');
        if (elem_type == 'file') {
            var f = getAncestorByTag(this.elem, 'form');
            if (f) {
                f.setAttribute('enctype', 'multipart/form-data');
            }
        }
    } else {
        error('bbfield used on non-form element '+this.elem.tagName);
    }


} // bbfield

// handle req_line variants of record update (for dg)
bb.bbfield_req_line = function(inWidgetElem, inModelID) {
    bb.bbfield.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbfield_req_line');

    var myname = inWidgetElem.name;
    // enter key disabled on item number until Tabbing (enter to tab) issues
    // resolved
    if (myname.match(/item_number\]$/)) {
        // indent
        window.info('item_number Enter key disabled for '+myname);
        inWidgetElem.addEventListener('keydown', _input_enterDisable, false);
    }

    // replace the 'regular' bb system _setValue
    // money gets special formatting
    if (myname.match(/_price\]$/) || myname.match(/_cost\]$/)) {
        // indent
        window.info('money input for '+myname);
        inWidgetElem.setValue = _input_setMoneyValue;

    }
    // w.register_observer(this.prefix, 'post_receive_update', window.update_totals, this);
} // bbfield_req_line

// handle pay_line variants of record update (for dg)
bb.bbfield_pay_line = function(inWidgetElem, inModelID) {
    bb.bbfield.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbfield_pay_line');

    // replace the 'regular' bb system _setValue

    // money gets special formatting
    var myname = inWidgetElem.name;
    if (myname.match(/_price\]$/) || myname.match(/_cost\]$/)) {
        // indent
        window.info('money input for '+myname);
        inWidgetElem.setValue = _input_setMoneyValue;

    }

    // w.register_observer(this.prefix, 'post_receive_update', window.update_totals, this);
} // bbfield_pay_line

// value of this field restricted to Y/N
bb.bbfield_yesno = function(inWidgetElem, inModelID) {
    bb.bbfield.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbfield_yesno');

    // Verify whether inField has a Y or N or empty value.
    // if not, complain.  To be called onblur
    this.html_yesno = function(inPrefix, inFeature, inArgList) {
        var w = inArgList;
        var va = w.elem.getValue();
        var evt = window.dispatcher.event_proceed;
        if (va.length > 0) {
            va = va.toUpperCase();
            if ((va != 'Y') && (va != 'N')) {
                report_error(w.elem, 'Only Y or N permitted');
                w.elem.setValue( w.data.old_value);
                w.elem.focus();
                evt = window.dispatcher.event_error;
            } else {
                w.elem.setValue( va );
            }
        }
        return evt;
    } // html_yesno
    var p = this.prefix;
    if (!p || p.length <= 0) {
        p = '_HEAD_';
    }
    window.dispatcher.register_observer(p, 'pre_queue_update', this.html_yesno, this);
    // this.dispatcher.register('blur', this.elem, this.html_yesno, this, 'blur_html_yesno.'+this.elem.name);
} // bbfield_yesno

// Enclose fields of one physical record.
// register event handlers on fields of the record
// A record has a primary key and table value that uniquely
// identify it in the system.  It will usually have
// a source_key/source_table and head_key/head_table values as well.
// A context or part_context value is expected to hint what
// this record is for.
// A prefix value
bb.bbrecord = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbrecord');

    this.table = null;
    this.key = null;
    this.source_key = null;
    this.source_table = null;
    this.sv_relation_master_id = null;
    this.head_key = null;
    this.head_table = null;

    // create and return a new, empty queue object
    this.create_queue = function (inArgList2) {
        var inArgList = clone(inArgList2);
        var queue = new Object();
        queue.view = '' + inArgList.table + '_aj_view';
        queue.action = '' + inArgList.table + '_aj_update';
        queue.display_mode = 'json';
        queue.head_key = inArgList.head_key?inArgList.head_key:'insert';
        queue.head_table = inArgList.head_table;
        queue.source_key = inArgList.source_key;
        queue.source_table = inArgList.source_table;
        queue.sv_relation_master_id = inArgList.sv_relation_master_id;
        queue.key = inArgList.key;
        queue.table = inArgList.table;
        queue.part_prefix = inArgList.part_prefix;

        // this.debug('new queue is '+to_string(queue));
        return queue;
    } // create_queue

    // Event
    // store changed data for later sending to server
    // usually clientside
    // inElement should have a .widget that is a bbfield
    this.queue_update = function(inElement, inArgList, inEvent) {
        var ok = true;
        var row_widget = inElement.widget.record;
        if (!row_widget.queue) {
            row_widget.queue = row_widget.create_queue(inElement.widget.data);
        }
        // support ad hoc error checking
        var evt = window.dispatcher.apply_feature(row_widget.part_prefix, 'pre_queue_update', row_widget);
        if (evt == window.dispatcher.event_error) {
            // Event handler must tell user of error.
            this.warn("Event pre_queue_update failed. Not queued.")
            ok = false;
        }

        if (ok) {
            row_widget.debug('queueing up '+inElement.name+' with value '+inElement.getValue());
            row_widget.queue[inElement.name] = inElement.getValue();

            var row = row_widget.elem;

            if ((inElement.name.indexOf('qty_ordered]') >= 0) &&
                (inElement.getValue() <= 0)) {
                // ### qty_ordered == 0 --> delete
                row_widget.queue_send(inElement, row_widget.queue, json_receive, inEvent);
            }
        }
        if (ok) {
            var evt = window.dispatcher.apply_feature(row_widget.part_prefix, 'post_queue_update', row_widget);
            if (evt == window.dispatcher.event_error) {
                //
                this.warn("Event post_queue_update failed.");
                return false;
            }
        }
        return false;

    } // queue_update

    // Event
    // destroy this record
    // trigger immediate send to server
    this.queue_delete = function(inElement, inArgList, inEvent) {
        var ok = true;
        var row_widget = inElement.widget.record;
        if (!row_widget.queue) {
            row_widget.queue = row_widget.create_queue(inElement.widget.data);
        }
        // support ad hoc error checking
        var evt = window.dispatcher.apply_feature(row_widget.part_prefix, 'pre_queue_delete', row_widget);
        if (evt == window.dispatcher.event_error) {
            // Event handler must tell user of error.
            this.warn("Event pre_queue_delete failed. Not queued.")
            ok = false;
        }
        if (ok) {
            var pfx = inElement.widget.prefix;
            row_widget.queue.action = inArgList.table + '_aj_delete';
            row_widget.queue[pfx + '[part_context]'] = 'delete';
            row_widget.queue_send(row_widget.elem, row_widget.json_dispatch, inEvent);
        }
        if (ok) {
            var evt = window.dispatcher.apply_feature(row_widget.part_prefix, 'post_queue_delete', row_widget);
            if (evt == window.dispatcher.event_error) {
                //
                this.warn("Event post_queue_update failed.");
            }
        }

        return false;
    } // queue_delete

    // Event
    // Compensate for not getting a reliable focus/blur/change
    // event on table row.
    // Track current row.  If it changes then do queue_send() on the
    // previously current row.
    // inElement is a bbfield widget.
    this.focus_record = function(inElement, inArgList, inEvent) {
        var elrow = inElement.widget.record.elem;
        var ctr = null;
        //  the 'current_record' is called 'focus_row'
        // from the day when records were always in a table
        // info('bbrecord.focus_record '+to_string(inArgList));
        if (!window.dispatcher.focus_row) {
            if (elrow) {
                window.dispatcher.focus_row = elrow;
                // debug('first focus on   '+elrow.tagName + ', '+elrow.className);
            }
        } else if (window.dispatcher.focus_row != elrow) {
            // change focus
            var old_row = window.dispatcher.focus_row;
            if (old_row.widget.queue_send) {
                if (old_row.widget.queue) {
                    old_row.widget.debug('old_row.item: '+inElement.getValue());
                    old_row.widget.queue_send(old_row, old_row.widget.json_dispatch, inEvent);
                }
            } else {
                // old approach
                window.dispatcher.warn('Using old global queue_send for '+old_row.tagName);
                queue_send(old_row, inArgList, json_receive, inEvent);
            }
            replace_class(old_row, 'hilight', 'norm');
            if (elrow.widget) {
                window.dispatcher.focus_row = elrow;
            } else {
                error('invalid record received focus: '+elrow.tagName);
            }
        }
        replace_class( window.dispatcher.focus_row, 'norm', 'hilight');

        var tl = window.dispatcher.get_widget('_HEAD_');
        if (tl.apply_feature) {
            // focus is a page-level property.
            tl.apply_feature('_HEAD_', 'global_leave_record', tl); // old_row
            tl.apply_feature('_HEAD_', 'global_focus_record', tl); // elrow
        }

    } // focus_record


    // Private
    // Action
    // index.php?view=aj_view&action
    // Sends a bbrecord's .queue property.
    // inElement should be a bbrecord element whose queue should
    // be sent to server
    // inCB is function to call with the response. typcially this.json_dispatch
    this.queue_send = function(inElement, inCB, inEvent) {
        var row = inElement;
        var w = inElement.widget;
        w.debug("bbrecord.queue_send("+row.tagName+",...,"+inEvent.type+")");
        if (row) {
            if (!row.widget.queue) {
                // nothing to send
                return false;
            }
            // ### synchronize?
            w.dispatcher.dispatch_aj(inEvent.type, inElement, background_send, inCB, row.widget.queue, 'queue_send');
            // background_send(row, row.widget.queue, inCB, inEvent);
            row.widget.queue = null;
            delete row.widget.queue;
        }
        return false;
    } // queue_send

    // Event
    // force a server synchronization.
    // do queue_update() and queue_send() without undue intrusion
    // on internals of bbrecord
    // call directly (elem.widget.record.force_update(elem));
    this.force_update = function(inElement) {
        var row = this.elem;
        var w = inElement.widget;
        w.debug("bbrecord.force_update("+row.tagName+",...,"+")");
        if (row) {
            this.queue_update(inElement, inElement.widget.data, {type:'change',target:inElement});
            window.dispatcher.dispatch_aj('change', row, background_send, row.widget.json_dispatch, row.widget.queue, 'Record Force Update');
            row.widget.queue = null;
            delete row.widget.queue;
        }
        return false;
    }

    // this.elem.field_list[] array of bbfield elements

    // Event Handler
    // queue_send() response handler
    // We expect a single record returned-- the record matching the
    // table,key that was submitted
    // inElement is a bbrecord
    // ### would like to accept any data presently displayed for updating
    this.json_dispatch = function(inReq, inArgs, inElement, inSuccess) {
        var w = inElement.widget;
        w.debug("bbrecord.json_dispatch");
        var ok = inSuccess?true:false;
        if (!ok) {
            w.error(" Timeout error: could not contact server ");
        }
        if (ok) {
            var stat = inReq.status;
            ok = (stat >= 200) && (stat < 300);
            if (!ok) {
                w.error(" HTTP error " + stat + " " + inReq.statusText);
            }
        }
        if (ok) {
            var jtxt = inReq.responseText;
            debug_server_reply(inReq, inArgs, inElement, inSuccess);
            var otxt = JSON.parse(jtxt);
            ok = (otxt instanceof Object) ;
            if (!ok) {
                w.error("Cannot create JSON object " + inReq.statusText);
            }
        }

        if (ok) {
            w._dispatch(otxt, inArgs);
        }
    } // json_dispatch

    // support json_dispatch().  xfer 'this' to our context
    // this is the DOM Element that received the event.
    this._dispatch = function (otxt, inArgs) {
        window.debug("bbrecord._dispatch otxt.receive: '"+otxt.receive+"'");
        var ok = true;
        var ep = window.dispatcher.event_proceed.toString();
        if (isset(otxt['aj_action_response'])
            && (otxt['aj_action_response'] != ep)) {
            // server had an error, dont 'proceed' with
            // usual processing.
            window.warn('server action failed in bbrecord::_dispatch()');
            //ok = false;
        } else {
            if ((otxt['receive'] == 'record_update') && (inArgs['key'] != 'insert')) {
                // show (and/or replace) an existing record
                ok = this.receive_update(otxt, inArgs);
            } else if ((otxt['receive'] == 'record_update') && (inArgs['key'] == 'insert')) {
                // ### compensate for server sending record_update
                // instead of record_insert when user typed in the 'insert' row
                // ### for new head record, we always get key
                ok = this.receive_insert(otxt, inArgs);
            } else if (otxt['receive'] == 'record_delete') {
                // remove record from display.
                ok = this.receive_delete(otxt, inArgs);
            } else if (otxt['receive'] == 'record_insert') {
                ok = this.receive_insert(otxt, inArgs);
            } else if (otxt['receive'] == 'choose_dialog') {
                ok = this.receive_dialog(otxt, inArgs);
            } else {
                this.error('Unknown receive command: '+otxt['receive']);
                ok = false;
            }
        } // if aj_action_response

        if (ok) {
            // multiple dispatch
            ok = this._dispatch_aj_receive(otxt, inArgs);
        } else {
            // alert ("no multidispatch");
        }
        return ok;

    } //_dispatch

    // private
    // support _dispatch()
    // handle multiple dispatch otxt['aj_receive'] parameter
    this._dispatch_aj_receive = function (otxt, inArgs) {
        var a = otxt['aj_receive'];
        // alert("Multiple dispatch");
        var ok = true;
        if (is_array(a)) {
            var recv_cmd = '';
            var recv_prefix = '';
            var i = 0;
            for(i = 0; i < a.length; i++) {
                recv_cmd = a[i]['receive'];
                window.info('multiple dispatch '+ recv_cmd);
                recv_prefix = a[i]['part_prefix'];
                if (recv_prefix) {
                    // find the object that recv_cmd applies to.
                    var w = window.dispatcher.get_widget(recv_prefix);
                    if (!w) {
                        // get_widget() complained already
                        w = this;
                    }
                } else {
                    // otherwise it applies to us
                    w = this;
                }
                if (w && recv_cmd && w[recv_cmd]) {
                    ok = w[recv_cmd](a[i], inArgs);
                } else {
                    this.error('multiple dispatch "'+ recv_cmd + '" not defined');
                }

            }
        }
        return true;
    }

    this.nop = function() { return true; }
    this.reload_page = function(inCmdArgs, inArgs) {
        var reload = document.getElementById('reload_page_form');
        bbtool.msgbox.show_message("Please wait...");
        if (reload) {
            // should be a form that will rebuild the current page.
            reload.submit();
        } else {
            // remove action=word from location
            var f = document.location.href;
            var re = /(.+?)action=[A-Za-z0-9_-]+(.*)/;
            var nf = f.replace(re, "$1$2");
            // window.alert('Loading url '+nf);
            document.location.assign(nf);
        }
    }

    // aj_receive handler
    this.show_message = function(inCmdArgs, inArgs) {
        bbtool.msgbox.show_message(inCmdArgs['message']);
        return true;
    }

    // Action
    // add new record to display
    // ### not for req_line
    // requires a widgetized 'insert' record, and 'this' is that record
    this.receive_insert = function(inResponse, inArgs) {
        this.warn('bbrecord.receive_insert testing');
        evt = window.dispatcher.event_proceed;

        var evt = window.dispatcher.apply_feature(this.part_prefix, 'pre_receive_insert', this);
        if (evt == window.dispatcher.event_error) {
            return false;
        } else if (evt == window.dispatcher.event_handled) {
            return true;
        }

        // ### other tables not adjusted to handle this
        if (inArgs.table && (inArgs.table != 'req_line_tag')) {
            this.receive_update(inResponse, inArgs);
        } else {
            this.create_row_data(inArgs, inResponse, true);
            this.create_row_view(inArgs, inResponse, true);
        }
        window.dispatcher.apply_feature(this.part_prefix, 'post_receive_insert', this);
        return true;
    } // receive_insert

    // View and Action
    // store changes to this record
    this.receive_update = function(inResponse, inArgs) {
        var row = this.elem;
        if (row) {
            var evt = window.dispatcher.apply_feature(this.part_prefix, 'pre_receive_update', this);
            if (evt == window.dispatcher.event_error) {
                return false;
            } else if (evt == window.dispatcher.event_handled) {
                return true;
            }
            this.fill_row(inArgs, inResponse, false);
            // ### HACK workaround external event handler dissappearing
            if (inResponse.table && (inResponse.table == 'address')) {
                if (window.lookup_tax_rate) {
                    this.debug('calling lookup_tax_rate()');
                    setTimeout(lookup_tax_rate, 100);
                }
            }

            if (inResponse.table &&
                ((inResponse.table == 'req_line') || (inResponse.table == 'pay_line') )) {
                // ### HACK req_line_js initialization is broken
                // 'calling window.update_totals');
                var qty_ordered_field = getElementMatchingName(row, 'lineqty_ordered]');
                window.dispatcher.dispatch('change', qty_ordered_field, window.update_totals, qty_ordered_field.widget.data, 'Selected Ship Point: Update Totals');
            }
            if (!inResponse.table) {
                this.error('no table in response: '+to_string_shallow(inResponse));
            }
            window.dispatcher.apply_feature(this.part_prefix, 'post_receive_update', this);
        } else {
            this.error("inElement is not in a bbrecord ("+inElement.tagName+") " + JSON.stringify(inResponse));
        }
        return true;
    } // receive_update

    // Self destruct
    this.receive_delete = function(inResponse, inArgs) {
        this.debug('bbrecord.receive_delete');
        row = this.elem;
        if (row) {
            var evt = window.dispatcher.apply_feature(this.part_prefix, 'pre_receive_delete', this);
            if (evt == window.dispatcher.event_error) {
                return false;
            } else if (evt == window.dispatcher.event_handled) {
                return true;
            }
            row.parentNode.removeChild(row);
            window.dispatcher.apply_feature(this.part_prefix, 'pre_receive_delete', this);

        } else {
            this.error("inElement is not in a bbrecord ("+inElement.tagName+") " + JSON.stringify(inResponse));
        }
        return true;
    } // receive_delete

    // show a dialog.
    // OBSELETE? dialogs generally handle this themselves
    this.receive_dialog = function(inResponse, inArgs) {
        this.error('bbrecord.receive_dialog not implemented');
    } // receive_dialog

    // Support receive_insert
    // Copy from inResponse to window.recordmap
    // under inResponse.new_part_prefix
    // Part of receive_insert
    this.create_row_data = function(inArgs, inResponse, inInsert) {
        // inResponse contains a new (to us) record;
        // 1. Construct part_prefix of insert record from part_prefix in inResponse
        // 2. Copy from recordmap[part_prefix of insert]
        //    to recordmap[part_prefix inResponse]
        // 3. Set primary key of copy, adjust parent of copy
        //    according to inResponse.

        this.warn('bbrecord.create_row_data ');
        // we are filling the insert row
        // convert insert row into regular row
        var ins_prefix = this.part_prefix;
        var ins_record = window.dispatcher.get_recordmap(ins_prefix); // this._get_recordmap();
        if (ins_record.part_prefix != this.part_prefix) {
            this.warn('bbrecord.insert record prefix not match current record');
            this.info("ins_record.part_prefix=" + ins_record.part_prefix);
            this.info("this.part_prefix=" + this.part_prefix);
            ins_record = clone(inResponse); // HACK
        }
        var send_prefix_name = inArgs['part_prefix'];
        var a = clone(ins_record); // clone(inArgs);
        a['key'] = inResponse['key'];

        // replace the last occurance of insert in field name with key value
        // var keyedfn_prefix = str_replace_last(ins_prefix, 'insert', a['key']);
        var keyedfn_prefix = inResponse['new_part_prefix'];
        // this.debug(to_string(inResponse));
        // var keyedfn_prefix =  inResponse.part_prefix
        // this.debug('Changing insert row to '+keyedfn_prefix+ ' from key '+ a['key']);
        a[ins_prefix] = undefined;
        delete a[ins_prefix];
        // a[send_prefix_name] = undefined;
        // delete a[send_prefix_name];
        a['part_prefix'] = keyedfn_prefix;
        a['action'] = a['table'] + '_aj_update';

        if (isset(window.recordmap[keyedfn_prefix])) {
            this.note("Record " + keyedfn_prefix + " already exists; clobbering ");
        }
        window.recordmap[keyedfn_prefix] = clone(a);

       return true;
    }   // create_row_data

    // duplicate the insert row, fill the original
    this.create_row_view = function(inArgs, inResponse, inInsert) {
        var insRow = this.elem;
        if (insRow.widget.queue) {
            delete insRow.widget.queue;
        }

        var tbl = insRow.parentNode;
        var new_row = insRow.cloneNode(true);
        tbl.appendChild(new_row);
        // cloneNode did not copy event handlers
        // DOM Event model does not allow inquiries about existing event handlers.

        this.fill_row(inArgs, inResponse, inInsert);
        // new_row becomes the insert row.
        if (inInsert) {
            // ### strictly, we only need widgetize new_row itself..
            // but widgetize does not affect the node passed in
            widgetize(new_row.parentNode);
        }
        return true;
    }   // create_row_view

    // support receive_insert, receive_update
    // copy from inResponse to our bbfields
    // if inInsert is true then change the field names
    // to match inResponse prefix and redo connect_to_data()
    // part of receive_update
    this.fill_row = function(inArgs, inResponse, inInsert) {
        var inRow = this.elem;
        if (inRow.widget.queue) {
            // We are about to blow away
            // everything on the line with a new/updated record, so
            // the queue contents are irrelevent
            // Clean up any pending changes to the old record
            delete inRow.widget.queue;
        }
        var nl_inputs = inRow.getElementsByTagName('input');
        var nl_selects = inRow.getElementsByTagName('select');
        var nl_textareas = inRow.getElementsByTagName('textarea');
        var nl_buttons = inRow.getElementsByTagName('button');
        var input_labels = inRow.getElementsByTagName('a');
        var inputs = new Array();
        var k = 0;
        append_nodelist(inputs, nl_inputs, true);
        append_nodelist(inputs, nl_selects);
        append_nodelist(inputs, nl_textareas);
        append_nodelist(inputs, nl_buttons);
        // debug_msg('nl_inputs.length: '+nl_inputs.length + ' inputs.length: '+inputs.length);
        var len = inputs.length;
        var evt_src_element_name = inArgs['part_prefix'];
        for (var i = 0; i < len; i++) {
            // extract aliasname
            try {
                if (inputs[i] && inputs[i].name) {
                    var pfxfn = inputs[i].name;
                    var alias_name = str_extract_alias_name(inputs[i].name);

                    // save value
                    if (inResponse['records'][0][alias_name] !== undefined) {
                        this.info('set '+alias_name+' = '+inResponse['records'][0][alias_name]);
                        inputs[i].setValue(inResponse['records'][0][alias_name]);
                    } else {
                        this.warn('alias_name not in record: ' + alias_name);
                    }

                    // save value label
                    var label_elem = document.getElementById(inputs[i].name);
                    if (label_elem) {
                        // Server specifies the label for a value
                        // by appending _description to field name
                        // ### if inResponse['records'][0][alias_name] not found,
                        // then there is no point in looking for a label
                        var label_alias = alias_name + '_description';
                        if (inResponse['records'][0][label_alias] !== undefined) {
                            window.info('found label elem '+inputs[i].name + ' labeled '+inResponse['records'][0][label_alias]);
                            label_elem.setLabel(inResponse['records'][0][label_alias]);
                        } else {
                            window.info('label '+label_alias+' not found, skipped ');
                            // label_elem.setLabel('----');
                        }
                    }
                    if (inInsert) {
                        // we are filling the insert row
                        // convert insert row into regular row

                        var keyedfn = str_replace_last(pfxfn, 'insert', inResponse['key']);
                        // var keyedfn_prefix = str_extract_prefix(keyedfn);
                        debug('Changing insert row to '+keyedfn+ ' from '+ pfxfn);
                        inputs[i].name = keyedfn;
                        if (inputs[i].widget && inputs[i].widget.isa('bbfield')) {
                            inputs[i].widget.connect_to_data();
                            window.debug(inputs[i].name + ' new data: '+to_string(inputs[i].widget.data));
                        }

                        // ### move outside the for i loop
                        for (k = 0; k < input_labels.length; k++) {
                            // we just duplicated the insert row
                            // rename the label so document.getElementById
                            // works again
                            if (input_labels[k].id == old_name) {
                                input_labels[k].id = keyedfn;

                                // probably fails in IE
                                break;
                            }
                        }

                        // window.pmap OBSELETE,

                    }
                } // if input
                else {
                    this.error('invalid element ['+i+']');
                }
            } catch(e) {
                this.error('exception '+e.toString());
            }
        } // for i
    } // fill_row

    // Our bbfields should have added themselves to the array
    // this.elem.field_list
    // register some record-level event handlers on them.
    this.post_widgetize = function () {
        if (!this.elem.field_list) {
            error('record with no fields! '+ this.elem.tagName);
            return;
        }
        var il = this.elem.field_list.length;
        // obtain prefix name for record
        if (il > 0) {
            this.part_prefix = this.elem.field_list[0].prefix;
            this.table = this.elem.field_list[0].data.table;
            var pfx = this.elem.field_list[0].prefix;
            if (pfx && (pfx != ''))  {
            } else {
                pfx = '_HEAD_';
            }
            // this.dispatcher.debug(this.table+' add_widget('+pfx.toString()+') for '+this.elem.className);
            this.dispatcher.add_widget(this, pfx);
        } else {
            this.dispatcher.warn('no fields for '+this.elem.className);
            this.dispatcher.add_widget(this, '_HEAD_');
            this.part_prefix = '';
        }

        var i = 0;
        for(i = 0; i < il; i++) {
            var d = this.elem.field_list[i].data;
            if (!d) {
                this.error('bbrecord: bbfield element has no data '+this.elem.field_list[i]);
            }
            this.elem.field_list[i].record = this;
            this.dispatcher.register('focus', this.elem.field_list[i].elem, this.focus_record, d, 'focus_bbfield_notify_record.'+this.elem.field_list[i].elem.name);
            this.dispatcher.register('change', this.elem.field_list[i].elem, this.queue_update, d, 'change_bbfield_queue_update.'+this.elem.field_list[i].elem.name);
            this.info('registered change ' + this.elem.field_list[i].elem.name);
        }

    } // post_widgetize

    // scan this.elem.field_list for element with field_name = inFieldName
    this.get_field_by_name = function (inFieldName) {
        var i = 0;
        var il = this.elem.field_list.length;
        for(i = 0; i < il; i++) {
            if (this.elem.field_list[i].data.field_name == inFieldName) {
                return this.elem.field_list[i];
            } else {
                //this.info("get_field_by_name: Not i: " + this.elem.field_list[i].data.field_name);
            }
        }
        this.warn("get_field_by_name: Not found: " + inFieldName);
        return false;
    }

    // bucket so UI code such as dialogs can stash info
    // about this record here.
    // known state items:
    // is_new_record: true if this record was aj_insert()ed during the
    //    current edit session (ie, has never been commit()ed).
    // ship_pt_dialog_shown: true if ship_pt dialog viewed at least once.
    this.set_ui_state = function(inName, inState) {
        if (!this.ui_state) {
            this.ui_state = new bb.bbobject();
        }
        this.debug('ui_state: "'+inName+'" = "'+inState+'".');
        this.ui_state[inName] = inState;
    }

    this.get_ui_state = function(inName, inState) {
        var out = false;
        if (this.ui_state && this.ui_state[inName]) {
            out = this.ui_state[inName];
        }
        return out;
    }

    bbtool.observable_mixin.apply(this, []);
}  // bbrecord

// a control that can receive focus and generate events, but
// cannot be edited by the user.
// Applied on an <a> tag.
bb.bblabel = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bblabel');

    // delegate change effects to the labeled field.
    inWidgetElem.setValue = function(newValue) {
        this.widget.get_field().setValue(newValue);
    }
    inWidgetElem.getValue = function() {
        return this.widget.get_field().getValue();
    }

    // manipulate the label
    inWidgetElem.setLabel = function(newValue) {
        this.innerHTML = newValue;
    }
    inWidgetElem.getLabel = function() {
        return this.innerHTML;
    }

    this.get_field = function() {
        return this.labeled_elem;
    }

    // name of the labeled form input, id of the label element
    this.name = this.elem.id;
    this.labeled_elem = null;

    // find the field that we are a label for
    this.post_widgetize = function() {
        var field = document.getElementsByName(this.name);
        if (field && field.length > 0) {
            this.labeled_elem = field[0];
        } else {
            this.error('cannot find field named '+this.name);
        }
        if (field.length >1) {
            this.warn('found '+field.length+' elements named '+this.name);
        }

    } // bblabel.post_widgetize
} // bblabel

// a control that can NOT receive focus and generate events,
// has a value associated
// BROKEN regarding datagrid insert but usable
bb.bbreadonly = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbreadonly');

    // delegate change effects to the labeled field.
    // should never happen, but..
    inWidgetElem.setValue = function(newValue) {
        var n = this.widget.get_field();
        if (n) {
            n.setValue(newValue);
        }
    }
    inWidgetElem.getValue = function() {
        var n = this.widget.get_field();
        if (n) {
            return n.getValue();
        }
        return false;
    }

    // manipulate the label
    inWidgetElem.setLabel = function(newValue) {
        this.innerHTML = newValue;
    }
    inWidgetElem.getLabel = function() {
        return this.innerHTML;
    }

    this.get_field = function() {
        return this.labeled_elem;
    }

    // assigned to this.labeled_elem.
    // update the label when labeled_elem.setValue happens.
    this._input_setHiddenReadOnlyValue = function(inValue) {
        this.value = inValue;
        var n = this.getAttribute('name');
        if (n) {
            var sp = to_element(n);
            sp.innerHTML = inValue;
        }
        return null;
    }

    // name of the labeled form input, id of the label element
    this.name = this.elem.id;
    this.labeled_elem = null;

    // find the field that we are a label for
    this.post_widgetize = function() {
        var field = document.getElementsByName(this.name);
        if (field && field.length > 0) {
            this.labeled_elem = field[0];
            // ### ugly, need chain
            this.labeled_elem.old_setValue = this.labeled_elem.setValue;
            this.labeled_elem._input_setHiddenReadOnlyValue = this._input_setHiddenReadOnlyValue;
            this.labeled_elem.setValue = function (inValue) {
                this.old_setValue(inValue);
                this._input_setHiddenReadOnlyValue(this.getValue());
            }
        } else {
            this.error('cannot find field named '+this.name);
        }
        if (field.length >1) {
            this.warn('found '+field.length+' elements named '+this.name);
        }

    } // bbreadonly.post_widgetize
} // bbreadonly


// A basic clickable/modifyable element
bb.bbcontrol = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbcontrol');

    // delegate change effects to the labeled field.
    if (!inWidgetElem.setValue) {
        inWidgetElem.setValue = function(newValue) {
            this.widget.value = newValue;
        }
    }
    if (!inWidgetElem.getValue) {
        inWidgetElem.getValue = function() {
            return this.widget.value;
        }
    }

    // manipulate the label
    inWidgetElem.setLabel = function(newValue) {
        this.innerHTML = newValue;
    }
    inWidgetElem.getLabel = function() {
        return this.innerHTML;
    }


    // name of the labeled form input, id of the label element
    this.name = this.elem.id;
    if (!this.name) {
        this.name = this.elem.class;
    }
    this.labeled_elem = null;

} // bbcontrol

// a save button
bb.bbcommit = function(inWidgetElem, inModelID) {
    bb.bbwidget.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbcommit');

    window.dispatcher.commit = 0;
    // 0 -> not committed
    // 1 -> waiting for flush
    // 2 -> committed

    this.form = getAncestorByTag(this.elem, 'form');

    // commit_select is an optional hidden input tag
    // that will be set to the name of the commit button that was
    // clicked.  Allows server to know which commit button was clicked
    // since the button itself is disabled (prevents double-submit)
    this.commit_select = false;
    if (this.form.commit_select) {
        this.commit_select = this.form.commit_select;
    }

    // wait for any AJ operations to complete
    // before submitting this.form
    this.wait_submit = function(inElem, inArgList, inEvent) {
        var w = inElem.widget;
        if ((w.dispatcher.commit > 0) && (w.dispatcher.commit < 3) && (window.request_queue.length > 1)) {
            info("commit queue size: " + window.request_queue.length + ' commit level: '+ w.dispatcher.commit);
            // put self back on queue
            w.dispatcher.dispatch('click', inElem, w.wait_submit, w, 'wait_submit_requeued');
            w.dispatcher.commit++;

            // failed in Seamonkey 1.0.6 with uncaught exception el
            // var el = this;
            // setTimeout(function() { el.wait_submit() }, 300);
        } else {
            // reload
            if (window.dispatcher.debug_hold_bbcommit) {
                // skip this commit, reset
                window.dispatcher.debug_hold_bbcommit = false;
                delete window.dispatcher.debug_hold_bbcommit;
                window.dispatcher.commit = 0;
                inElem.disabled=false;
                return;
            }
            window.dispatcher.commit = 2;
            error('commit submit disabled for debugging');
            w.form.submit();
            // this.disabled=false;
        }
    } // wait_submit

    this.flush = function() {
        // set commit_select
        if (this.commit_select) {
            this.commit_select.disabled = false; // just in case
            this.commit_select.setValue(this.elem.name);
        }

        // clear all dialogs
        // nonstandard dlgs
        var d = to_element('choose_dialog');
        if (d) { d.widget.erase(); }
        var d2 = to_element('inventory_dialog');
        if (d2) { d2.widget.erase(); }
        var d3 = to_element('ship_pt_dialog');
        if (d3) { d3.widget.erase(); }

        // bbdhtml_dialogs (standard dlgs)
        var ds = bbtool.dialog.get_all_dialogs();
        if (ds && ds.length) {
            var ic = ds.length;
            var it = null;
            for(var i = 0; i < ic; i++ ) {
                it = ds.item(i);
                if (it && it.widget && it.widget.erase) {
                    it.widget.erase();
                }
            }
        }

        // find all bbrecords that have queues
        // do bbrecord.queue_send
        var records = getElementsByClassName(document, "*", 'bbrecord');
        var rc = records.length;
        var r = 0;
        for(r = 0; r < rc; r++) {
            // bbrecords loaded into a bbdhtml_dialog may not be widgetized.
            // Not all bbrecords are editable.
            if (records[r].widget && records[r].widget.queue) {
                try {
                    info('commit flushing '+to_string(records[r].widget.queue));
                    var fev = {type: 'blur'} ;
                    // this.dispatcher.dispatch('blur', records[r], records[r].widget.queue_send, records[r].widget.json_dispatch, 'commit flush');
                    records[r].widget.queue_send(records[r], records[r].widget.json_dispatch, fev);
                } catch (e) {
                    info('commit failed to flush '+records[r].tagName);
                }
            } else {
                info('commit skipping '+records[r].tagName);
            }

        }
        this.dispatcher.dispatch('click', this.elem, this.wait_submit, this, 'wait_submit_queued');
        // this.wait_submit();
    } // flush

    // submit form
    this.commit = function(inElem, inArgList, inEvent) {
        if (window.dispatcher.commit == 0) {
            window.dispatcher.commit = 1;
            this.disabled=true;
            // disable other commit buttons?
            this.widget.flush();
            // this.widget.deactivate();
        }
    } // commit

    this.dispatcher.register('click', this.elem, this.commit, this, 'click_commit');
} // bbcommit

// an observable commit button
// observe pre_commit_x
// a non-commit button would be handy.. but we dont have those.
bb.bbconfirm_commit = function(inWidgetElem, inModelID) {
    bb.bbcommit.apply(this, [inWidgetElem, inModelID]);
    this.add_isa('bbconfirm_commit');

    // submit form
    this.parent_commit = this.commit;
    this.commit = function(inElem, inArgList, inEvent) {
        var w = inArgList;
        var pre_ctxt = 'pre_' + inElem.name;
        var our_prefix = null; // commit buttons belong to head record

        // follow bbrecord convention of using window.dispatcher events
        var evt = window.dispatcher.apply_feature(our_prefix, pre_ctxt, w);
        if (evt == window.dispatcher.event_proceed) {
            window.info('confirm_commit proceed');
            return w.parent_commit(inElem, inArgList, inEvent);
        } else if (evt == window.dispatcher.event_handled) {
            window.info('confirm_commit cancelled');
            return true;
        } else if (evt == window.dispatcher.event_error) {
            window.info('confirm_commit failed');
        }

        window.dispatcher.commit = 0;
        return true;
    } // commit

    this.dispatcher.unregister('click', this.elem, this.parent_commit);
    this.dispatcher.register('click', this.elem, this.commit, this, 'click_confirm_commit');
    window.info("confirm_commit for '"+inWidgetElem.name+"'");
} // bbconfirm_commit

// Scan inDocument for all elements with a class attribute
// of .bb<something>.  Call the bb<something>
// widget constructor on it.
function widgetize(inDocument) {

    window.dispatcher.debug('widgetize ' + inDocument.title );
    var known_widgets   = new Array();
    var known_widget_fn = new Array();
    for (j in window.bb) {
        if (is_function(window.bb[j]) && (j != 'bbwidget')) {
            known_widgets.push(j);
            known_widget_fn.push(window.bb[j]);
        }
    }
    window.dispatcher.debug('known_widgets '+known_widgets.toString());

    var widget_instances  = new Array();
    var ic = known_widgets.length;
    for(var i = 0; i < ic; i++ ) {
        var vl = getElementsByClassName(inDocument, "*", known_widgets[i]);
        var jc = vl.length;
        window.info('    found '+jc+ ' widgets of type '+known_widgets[i]);
        if (jc == 0) { continue; }
        for (var j = 0; j < jc; j++) {
            if (!vl[j].widget) {
                var x = new known_widget_fn[i](vl[j], null);
                // the widget ref is permenantly stored in the DOM in vl[j].widget
                widget_instances.push(x);
            }
        }
    }

    // post_create
    var jc = widget_instances.length;
    for(var j = 0; j < jc; j++ ) {
        widget_instances[j].post_widgetize();
    }
    return true;

} // widgetize

// onload handler
function bbinit() {
    var load_time = Date.now();
    window.dispatcher = new window.bb.dispatcher();
    if (window.bbkeymap_init) {
        bbkeymap_init(window);
    } else {
        window.debug(window.name + ' is missing');
        window.error('cannot find bbkeymap_init');
    }
    rationalize_form_elements();
    window.widgetize(window.document);
    window.debug('Gecko Version: ' + geckoGetRv());
    var init_time = Date.now() - load_time;
    window.debug('bbinit time (ms): ' + init_time);

    return true;
} // bbinit

// convenience functions
// show bbwindow with id=inWinID
function show_window(inWinID) {
    var e = document.getElementById(inWinID);
    if (e && e.widget) {
        e.widget.show();
        e.widget.activate();
    } else {
        window.error("cannot find window '"+inWinID+"'");
    }
    return false;
} // show_window

// hide bbwindow with id=inWinID
function hide_window(inWinID) {
//    bbkeymap_init(window);
    var e = document.getElementById(inWinID);
    if (e) {
        e.widget.hide();
    } else {
        window.debug("cannot find window '"+inWinID+"'");
    }
    return false;
} // hide_window


// Assign consistent accessors to form element prototypes.
// Minimize JS side interface differences among form elements.
function rationalize_form_elements(inWidgetElem) {

    // This only works in Moz/FF.  Others dont have <Builtin>.prototype
    HTMLInputElement.prototype.setValue = _input_setValue;
    HTMLInputElement.prototype.getValue = _input_getValue;
    HTMLSelectElement.prototype.setValue = _select_setValue;
    HTMLSelectElement.prototype.getValue = _select_getValue;
    HTMLTextAreaElement.prototype.setValue = _textarea_setValue;
    HTMLTextAreaElement.prototype.getValue = _textarea_getValue;
    HTMLButtonElement.prototype.setValue = _input_setValue;
    HTMLButtonElement.prototype.getValue = _input_getValue;

    /*
     * Disabled bc FF does not permit assigning to keyCode
     * Different approach required
    // Enter Key ==> tab key for input and textarea

    var inputs = document.getElementsByTagName('input');
    var textareas = document.getElementsByTagName('textarea');
    var ic = inputs.length;
    for(var i = 0; i < ic; i++) {
        inputs[i].addEventListener('keydown', _input_enterTab, false);
    }
    var tc = textareas.length;
    for(var t = 0; t < tc; t++) {
        textareas[t].addEventListener('keydown', _input_enterTab, false);
       }
     */
    return true;
} // rationalize_form_elements

// Private
function _input_enterDisable(inEvent) {
    if (inEvent && inEvent.keyCode) {
        if(inEvent.keyCode == 13) {
            inEvent.preventDefault();
            inEvent.stopPropagation();

        }
    }
}
// Private
// map enter key to tab key for input elements
function _input_enterTab(inEvent) {
    if (inEvent && inEvent.keyCode) {
        if(inEvent.keyCode == 13) {
            // inEvent.keyCode = 9; // JS error:  setting a property that only has a getter
            // inEvent.preventDefault();
            //inEvent.stopPropagation();
            //window.dispatcher.warn('Enter keypress Aborted');
        }
    }
} // _input_enterTab
// Private
// non-anonymous functions assigned as accessors to form
// element prototypes.  Made non-anonymous to support debugging.
function _input_setValue(inValue) {
    this.value = inValue;
    return null;
}

function _input_getValue() { return this.value; }

function _input_setMoneyValue(inCash) {
    this.value = window.bbtool.t_money.money_print(inCash);
    return null;
}
function _textarea_setValue(inValue) {
    this.value = inValue;
    return null;
}
function _textarea_getValue() { return this.value; }

function _select_setValue(inValue) {
    // ### not for <select multiple>
    var found = false;
    for (var i = 0; i < this.options.length; i++) {
        if (this.options[i].value == inValue) {
            this.options[i].selected = true;
            found = true;
        }
    }
    if (found) {
    //    this.invoke_change();
    }
    return null;
}

function _select_getValue() {
    // ### not for <select multiple>
    for (var i = 0; i < this.options.length; i++) {
        if (this.options[i].selected) {
            return this.options[i].value;
        }
    }
    return null;
}

window.bbtool = new Object();

// JS version of PHP's core/control/metadata.php
// metadata class.  All information that can be inferred about a
// modular class in one package
bbtool.metadata = function(inTable) {
    var KEY_SUFFIX = '_id';
    var TEMPLATE_SUFFIX = '_template';
    var FEATURE_SUFFIX = '_feature';
    var MASTER_SUFFIX = '_master';
    var JS_CLASS_SUFFIX = '_js';

    this.table = inTable;
    this.key = inTable + KEY_SUFFIX;
    this.sequence = inTable + '_id_seq';
    this.template = inTable + TEMPLATE_SUFFIX;
    this.template_key = inTable + TEMPLATE_SUFFIX + KEY_SUFFIX;
    this.template_sequence = inTable + TEMPLATE_SUFFIX + '_id_seq';
    this.feature = inTable + FEATURE_SUFFIX;
    this.feature_key = inTable + FEATURE_SUFFIX + KEY_SUFFIX;
    this.feature_sequence = inTable + FEATURE_SUFFIX + '_id_seq';
    this.master = inTable + MASTER_SUFFIX;
    this.master_key = inTable + MASTER_SUFFIX + KEY_SUFFIX;
    this.master_sequence = inTable + MASTER_SUFFIX + '_id_seq';
    this.hier_seq = inTable + '_seq';
    this.observer = inTable + JS_CLASS_SUFFIX;

} // bbtool.metadata

// factory for metadata
bbtool.metadata.g = function(inTable) {
    var tname = '_C_' + inTable;
    if (!bbtool.metadata[tname]) {
        bbtool.metadata[tname] = new bbtool.metadata(inTable);
    }
    return bbtool.metadata[tname];
}


// remember which element last had focus, restore it when done.
// use: var what = new focus_hold(someDOMobj);
//      what.focus();
bbtool.focus_hold = function(inElem) {
    if (!inElem.focus) {
        window.dispatcher.error('cannot hold focus of '+inElem.tagName);
        return;
    }

    this.hold_elem = inElem;

    if (inElem.widget && inElem.widget.isa('bbfield')) {
        this.label = 'Restore Focus to ' + inElem.widget.name;
    } else {
        this.label = 'Restore Focus to ' + inElem.tagName;
    }

    // Private
    // actual handler
    this._handle_focus = function(inElement, inArgs, inEvent) {
        inElement.focus();
    }

    this.focus = function() {
        window.dispatcher.dispatch('focus', this.hold_elem, this._handle_focus, this.hold_elem, this.label);
    }
} // bbtool.focus_hold

// on creation:
// convert data in inResultSet into a <table>
// using mod_class standard controls
bbtool.result_table = function(inResultSet, inLabel, inParams) {

    /* inLabel format: 2D array as used by sv_dictionary
     *    additonally, format can be set to control HTML output (editable?)
     * array( 0 => array( 'field_name' => value
     *                    'alias_name' => value
     *                    'label' => value
     *                    'short_label' => value
     *
     *                    'format' => value
     *                    'prefix_name' => value
     */
    /* inResultSet format: 2D array as used by various mod_class tables
     * fields named by alias_name's value from inLabel
     */
    this.name = 'name';
    this.html = '<table border="0" id="'+this.name+'" class="result_set">';

    // dictionary label configuration
    this.label_column = 'short_label';
    this.title_column = 'label';
    this.field_column = 'field_name'; // used to retrieve data from field of result set
    this.alias_column = 'field_name'; // used to send data to server
    this.table_column = 'table_name'; // table for resultset

    // interface control configuration
    this.field_class = 'bbfield';
    this.record_class = 'bbrecord';
    this.result_class = this.name;

    // issues
    // create insert row (user can add new records to list)
    // fix prefix usage,         'table_prefix' = 'prefix[to][use]'
    //   the key is taken from inResultSet and appended to table_prefix as needed
    //   to form a record prefix
    // add records to record map so widgetize will work: 'add_recordmap' = true
    // add insert row            'add_insert_row' = true
    this.table_prefix = '';
    this.add_recordmap = false;
    this.add_insert_row = false;

    // source_* can arguably be discovered.
    this.source_key = false;
    this.source_table = false;

    this.result_set = inResultSet;
    this.labels = inLabel;

    // override any of the above
    if (inParams) {
        var k = 0;
        for(k in inParams) {
            this[k] = inParams[k];
        }
    }


    // properly construct prefix name for field
    // given prefix for record
    this.pfn = function(inPrefix2, inTable, inLeafName) {
        if (inPrefix2 && (inPrefix2.length > 0) && inPrefix2 != '_HEAD_') {
            return inPrefix2 + "[" + inTable + inLeafName + "]";
        } else { // head record
            return inTable + inLeafName;
        }
    }

    // this.get_html = function() { return this.html; }

    this.format_display_only = function(inData) { return inData; }
    this.format_input_text = function(inData, inLabel, inRecPrefix) {
        return "<input type='text' class='"
                  + this.field_class+"' size='4' name='"
                  + this.pfn(inRecPrefix, '', inLabel[this.alias_column])+
                  "' value='" + inData
                  + "' autocomplete='off'>";
    }
    this.format_input_text_ro = function(inData, inLabel, inRecPrefix) {
        return "<input type='text' class='"
                  + this.field_class+"'  name='"
                  + this.pfn(inRecPrefix, '', inLabel[this.alias_column])+
                  "' value='" + inData
                  + "' autocomplete='off' readonly='readonly'>";
    }
    this.format_input_hidden = function(inData, inLabel, inRecPrefix) {
        return "<input type='hidden'  class='"
                  + this.field_class + "' name='"
                  + this.pfn(inRecPrefix, '', inLabel[this.alias_column]) +
                  "' value='"+inData+"'>";
    }

    // format function selector
    this.apply_format = function(inFormat, inData, inLabel, inRecPrefix) {
        var ctxt = '';
        switch(inFormat) {
        case 'input_text':
            ctxt = ctxt + this.format_input_text(inData, inLabel,
                                                 inRecPrefix);
            break;
        case 'input_money':
            ctxt = ctxt + this.format_input_text(inData, inLabel,
                                                 inRecPrefix);
            break;
        case 'input_hidden':
            ctxt = ctxt + this.format_input_hidden(inData, inLabel,
                                                   inRecPrefix);
            break;
        case 'money':
            ctxt = ctxt + this.format_display_only(inData, inLabel,
                                                   inRecPrefix);
            break;
        case 'raw':
        default:
            ctxt = ctxt + this.format_display_only(inData, inLabel,
                                                   inRecPrefix);
            break;
        }
        return ctxt;
    } // apply_format

    this.get_html = function() {
        var inResultSet = this.result_set;
        var inLabel = this.labels;

        var num_results = inResultSet?inResultSet.length:0;
        var num_labels = inLabel.length;
        var txt = '';
        var ctxt = '';

        // heading
        ctxt  = '';
        for(var j = 0; j < num_labels; j++) {
            ctxt = ctxt + "<th title='" + inLabel[j][this.title_column] +"'>" + inLabel[j][this.label_column] + "</th>";
        }
        this.html = this.html + '<tr>' + ctxt + '</tr>';

        // body of data
        txt = '';
        for (var i = 0; i < num_results; i++) {
            txt = txt + '<tr id="' + this.name + i + '" class="' + this.record_class + '" >';
            ctxt = '';
            var rectm = bbtool.metadata.g(inLabel[0][this.table_column]);
            var rec_prefix = this.pfn(this.table_prefix, '', inResultSet[i][ rectm.key ]);
            for(var j = 0; j < num_labels; j++) {
                ctxt = ctxt + '<td id="'+ this.name + i +'_'+ j + '">';
                if (inLabel[j]['format']) {
                    ctxt = ctxt + this.apply_format
                              (inLabel[j]['format'],
                               inResultSet[i][ inLabel[j][this.field_column] ],
                               inLabel[j],
                               rec_prefix);
                } else {
                    ctxt = ctxt +  this.format_display_only
                              (inResultSet[i][ inLabel[j][this.field_column] ],
                               inLabel[j],
                               rec_prefix);
                }
                ctxt = ctxt + '</td>';
            }
            txt = txt + ctxt + '</tr>';

        }

        // insert row
        if (this.add_insert_row) {
            txt = txt + '<tr id="' + this.name + i + '" class="' + this.record_class + '" >';
            ctxt = '';
            var rectm = bbtool.metadata.g(inLabel[0][this.table_column]);
            var rec_prefix = this.pfn(this.table_prefix, '', 'insert');
            for(var j = 0; j < num_labels; j++) {
                ctxt = ctxt + '<td id="'+ this.name + i +'_'+ j + '">';
                if (j == 0) {
                    // insert machinery
                    ctxt  = ctxt + this.html_insert_hidden_fields
                              (rec_prefix,
                               inLabel[j][this.table_column]);
                }

                if (inLabel[j]['insert_format']) {
                    ctxt = ctxt + this.apply_format
                              (inLabel[j]['insert_format'],
                               '',
                               inLabel[j],
                               rec_prefix);
                } else {
                    ctxt = ctxt +  this.format_input_text
                              ('',
                               inLabel[j],
                               rec_prefix);
                }
                ctxt = ctxt + '</td>';
            }
            txt = txt + ctxt + '</tr>';
        }
        this.html = this.html + txt + '</table>';
        return this.html;
    } // get_html

    // copy result_set into window.recordmap
    this.update_recordmap = function() {
        // incomplete
    }

    // return the hidden fields required for successful aj_insert
    this.html_insert_hidden_fields = function(inRecPrefix, inTable) {
        var rec_html = '';
        var head_key = document.getElementsByName('head_key')[0];
        var head_table = document.getElementsByName('head_table')[0];
        var prefix = inRecPrefix;
        var tm = bbtool.metadata.g(inTable);
        var skey = this.source_key;
        var stable = this.source_table;
        var parts = bbtool.prefix_to_array(prefix);
        var relmaster = parts[ parts.length - 2];

        // extra decls for insert row machinery.
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'key') +"' value='insert' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'table') + "' value='" + tm.table + "' class='bbfield'>";

        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'source_key') + "' value='"+ skey +"' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'source_table') + "' value='"+ stable +"' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'sv_relation_master_id') + "' value='" + relmaster +"' class='bbfield'>";

        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'head_key') +"' value='" + head_key.getValue() + "' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'head_table') + "' value='" + head_table.getValue() + "' class='bbfield'>";

        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, tm.table, tm.key) + "' value='insert' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'template') +"' value='1' class='bbfield'>";
        rec_html += "<input type='hidden' name='"+ this.pfn(prefix, '', 'template_construct') +"' value='by_template' class='bbfield'>";

        // update recordmap for new record so bbrecord.post_widgetize() works
        window.recordmap[prefix] = {
            view:tm.table + '_aj_view',
            action:tm.table + '_aj_insert',
            display_mode:'json',
            key:'insert',
            table:tm.table,
            head_key:head_key.getValue(),
            head_table:head_table.getValue(),
            source_key:skey,
            source_table:stable,
            sv_relation_master_id:relmaster,
            template:1, // this.get_template_for_mode(),
            template_construct:'by_template',
            part_prefix:prefix
        }
        return rec_html;
    }

} // bbtool.result_table

// support local keymaps
// In key_manager, the 'this' for key handlers is the inElement registered
// with the handler. Same as with window.dispatcher event handlers
// TODO: replace window.keymap (bbkeymap) with instance of this class
bbtool.key_manager = function(inTopElem, inParam) {
    this.elem = inTopElem;
    this._local_keys = new Object();
    this.name = 'Key Manager';

    // Private
    // keyboard event handler
    // look up which _local_key was pressed, call it
    this._keystroke = function(inElem, inArgs, inEvent) {
        // list_dispatch has put _keystroke on the queue already,
        // so no need to use serialize.add().
        var reg_keys = inArgs._local_keys;
        var k = window.keymap.key_event_to_key(inEvent);
        var type = inEvent.type;
        if (reg_keys && k) {
            if ( (ie4 && reg_keys[k.sym] && (k.ie_event == inEvent.type))
                 || (nn6 && reg_keys[k.sym] && (k.ff_event == inEvent.type))) {

                reg_keys[k.sym].invoke(inEvent);
            }
        }
    } // _keystroke

    // add a function handler for a local key
    this.register_key = function(inKeySym, inElement, inHandler, inArgs, inLabel) {
        var k = inKeySym;
        if (typeof(inKeySym) == 'object') {
            k = inKeySym.sym;
        }
        var label = this.name + ' ' + k;
        if (inLabel) {
            label = inLabel;
        }

        this._local_keys[k] = new window.dispatcher.sequential_handler('keystroke', inElement, inHandler, inArgs, label);

    }

    // remove a key from local handler
    this.unregister_key = function(inKeySym, inElement) {
        var k = inKeySym;
        if (typeof(inKeySym) == 'object') {
            k = inKeySym.sym;
        }
        this._local_keys[k] = undefined;
        delete this._local_keys[k];
    } // unregister_key

    // install key event handlers on this.elem
    this.initialize = function() {
        window.dispatcher.register('keydown', this.elem, this._keystroke, this, this.name + ' keydown');
        window.dispatcher.register('keypress', this.elem, this._keystroke, this, this.name + ' keypress');
        window.dispatcher.register('keyup', this.elem, this._keystroke, this, this.name + ' keyup');
    }
    this.initialize();
} // bbtool.key_manager


// Install arrowkey handlers for navigating around the table inTopElem
bbtool.arrow_key_manager = function(inTopElem, inParam) {
    bbtool.key_manager.apply(this, [inTopElem, {}]);

    this.name = 'Arrow Manager';
    this.row_tag = 'tr'; // rows/vertical position tracked by this tag
    this.col_tag = 'input'; // horizontal position tracking
    this.row_first = 1;  // 0 assumed header row
    this.row_current = 1;// default row/currently selected row
    this.do_ui_initialize = true; // whether to put focus on first row immediately
    window.dispatcher.debug('creating '+this.name+' on '+this.elem.tagName);
    // override any of the above
    if (inParam) {
        var k = 0;
        for(k in inParam) {
            this[k] = inParam[k];
        }
    }

    this.vertical_list = inTopElem.getElementsByTagName(this.row_tag);
    this.row_last = this.vertical_list.length - 1;
    this.row_prev = this.row_current;   // last selected row

    if (this.row_last-1 < this.row_first) {
        // nothing to navigate;
        window.dispatcher.info(this.name+': nothing to navigate');
    } else {
        window.dispatcher.info(this.name+': navigating '+this.row_last +' rows');
    }


    // show user the currently selected row
    this.highlight_row = function(inElem) {
        replace_class(inElem, 'norm', 'hilight');
    }
    this.unhighlight_row = function(inElem) {
        replace_class(inElem, 'hilight', 'norm');
    }

    // Event
    // handle up arrow
    this.key_up = function (inTopElem, inArgs, inEventObj) {
        inArgs._key_up(inTopElem, inEventObj); // this
    } // key_up

    // Private
    this._key_up = function(inTopElem, inEventObj) {
        // adapt to deletion/insertion of this.row_tag elements
        this.row_last = this.vertical_list.length - 1;
        var c = this.row_current;
        c--;
        if (c < this.row_first) {
            c = this.row_first;
        } else if (c > this.row_last){
            c = this.row_last;
        }
        this.goto_row(c);
    } // _key_up

    // Event
    // handle down arrow
    this.key_down = function(inTopElem, inArgs, inEventObj) {
        inArgs._key_down(inTopElem, inEventObj); // this
    }

    // Private
    this._key_down = function(inTopElem, inEventObj) {
        // adapt to deletion/insertion of this.row_tag elements
        this.row_last = this.vertical_list.length - 1;
        var c = this.row_current;
        c++;
        if (c < this.row_first) {
            c = this.row_first;
        } else if (c > this.row_last){
            c = this.row_last;
        }

        this.goto_row(c);
    } // key_down

    // Event
    // sync selected row by arrow key with selected row by mouse click
    // registered on row_tag elements
    this.mouse_click = function(inTopElem, inArgs, inEventObj) {
        inArgs._mouse_click(inTopElem, inEventObj); // this
    }
    this._mouse_click = function(inTopElem, inEventObj) {

        var clicked = getAncestorByTag(inEventObj.target, this.row_tag);
        if (clicked) {
            clicked['_mark'] = 1;
            var found = -1;
            for(var i = this.row_first; i <= this.row_last; i++) {
                if (this.vertical_list[i]['_mark']) {
                    found = i;
                    break;
                }
            }
            clicked['_mark'] = undefined;
            delete clicked['_mark'];
            if (found >= this.row_first)  {
                this.goto_row(found, -1);
                if (clicked.focus) {
                    clicked.focus();
                }
                if (clicked.select) {
                    clicked.select();
                }

            }
        }
    } // arrow_key_manager.mouse_click

    // Protected
    // make c the current row
    this.goto_row = function(inRow, inCol) {
        var r = inRow;
        var col = null;
        if ((inCol === undefined) || (inCol === null)) {
            col = -2;
        } else {
            col = inCol;
        }
        this.unhighlight_row(this.vertical_list[this.row_current]);
        this.highlight_row(this.vertical_list[r]);
        this.row_current = r;
        // this.vertical_list[this.row_current].scrollIntoView(true);
        var lis = this.vertical_list[this.row_current].getElementsByTagName(this.col_tag);
        if (col >= 0) {
            // caller specified column offset
            window.dispatcher.error('caller-specified column pos not implemented');
        } else if (col <= -1) {
            // do not focus column
        } else {
            // focus first visible column
            // ### HACK should find column'th focusable tag
            var el = null;
            if (lis.length > 0) {
                var i = 0;
                el = lis[i];
                while(i < lis.length && (!(lis[i].focus && (lis[i].type != 'hidden')))) {
                    i++;
                }

                if (lis[i].focus) { lis[i].focus(); }
                if (lis[i].select) { lis[i].select(); }
            }
        }
        window.dispatcher.info(this.name + ': current_row = '+this.row_current);
    } // arrow_key_manager.goto_row

    // API
    this.unregister = function() {
        this.unregister_key(key.Au, this.elem);
        this.unregister_key(key.Ad, this.elem);
        window.dispatcher.unregister('click', this.elem, this.mouse_click);
    }

    // API
    this.register = function() {
        this.register_key(key.Au, this.elem, this.key_up, this, this.name + ' Up');
        this.register_key(key.Ad, this.elem, this.key_down, this, this.name + ' Down');
        window.dispatcher.register('click', this.elem, this.mouse_click, this, this.name + ' Click');
    }

    // API
    this.goto_first_row = function() {
        this.row_last = this.vertical_list.length - 1;
        if (this.vertical_list[this.row_first]) {
            this.elem.scrollTop = 0;
            this.row_current = this.row_first;
            this.goto_row(this.row_current);
        }
    }

    // main
    // this.register();

    // initialize UI on first row.
    if (this.do_ui_initialize) {
        if (this.vertical_list[this.row_current]) {
            this.goto_row(this.row_current);
        }
    }
} // bbtool.arrow_key_manager


bbtool.observable_mixin = function() {
    // mixin has no need to inherit from bbobject
    // this.add_isa('observable_mixin');

    /* idea: provide class/template/record-specific behavior via observers
     * on bbrecord and bbfield
     * Exactly what can be observed is unclear.
     * - cannot change/remove the default behavior.
     * + works like the _feature tables in PHP (but functions only).
     * + more than one observer on a record.
     * + can make up new things to observe at will
     * + supports record-specific effects where _feature does not
     */

    /* rep
     *
       this.observemap[prefixname]={
        feature_name1:[fn1, fn2, ...]
        feature_name2:[fn1, fn2, ...]
       }
     * prefixname allows us to match objects to observer functions
     * without needing an instance of the object
     */
    this.observemap = new Object();

    /* deploy js
     * in some onload handler do
       register_observe(prefix_name, feature_name, func, args)
       adds func to the observemap[prefix_name].feature_name list
       func will be called with args prefix_name, feature_name, args
       in context of widget
       as though it were defined as widget.func()

     */

    /* deploy php
     * _feature pre_whatever
     * create 1 or more formatted fields named like 'js_observe.*'
     *  which by various means causes
     * some javascript functions to be defined (once)
     * and does something like
     * register_onload(function () { register_observe(...) })
     *
     * reasonable content for this formatted field:
     * <script src='url'></script>
     * <script>code</script>
     *
     * JS lacks any notion like PHP include_once.
     */
    this.event_proceed = 1;
    this.event_abort = -1;
    this.event_handled = -1;
    this.event_error = 0;
    this.event_default_prefix = '_HEAD_';
    // Call observers of inFeature on record with inPrefix in context of inWidget
    this.apply_feature = function(inPrefix, inFeature, inWidget) {
        var prefix = inPrefix;
        if (!prefix) {
            prefix = '_HEAD_';
        }
        window.dispatcher.debug('apply_feature: ' + inFeature + ' to '+prefix);
        var evt = this.event_proceed;

        // ### wildcard feature?
        if (this.observemap[prefix] && this.observemap[prefix][inFeature]) {
            var flist = this.observemap[prefix][inFeature];
            var ic = flist.length;
            for(var i = 0; i < ic; i++) {
                if (!(flist[i] && flist[i].args && flist[i].args.name)) {
                    window.dispatcher.error('Invalid feature skipped for ' + inFeature + ' (unregister_event() called within an event handler?)');
                    continue;
                }
                window.dispatcher.debug('apply_feature: '+ inFeature +' calling ' +flist[i].args.name);
                // ### dispatch queue?
                evt = flist[i].func.apply(inWidget, [inPrefix, inFeature, flist[i].args]);
                if ((evt === this.event_abort) || (evt === this.event_error)) {
                    return evt;
                }
                if ((evt === null) || (evt === undefined)) {
                    // flist[i].func did not return a value
                    evt = this.event_proceed;
                }
            }
        }
        return evt;
    } // apply_feature

    this.register_observer = function(inPrefix, inFeature, inFunc, inArgs) {
        if (!this.observemap[inPrefix]) {
            this.observemap[inPrefix] = new Object();
        }
        if (!this.observemap[inPrefix][inFeature]) {
            this.observemap[inPrefix][inFeature] = new Array();
        }
        var f = {
            func:inFunc,
            feature:inFeature,
            prefix:inPrefix,
            args:inArgs
        };
        this.info('register_observer() of '+inPrefix+' looking for '+inFeature);
        this.observemap[inPrefix][inFeature].push(f);
    } // register_observer

    this.unregister_observer = function(inPrefix, inFeature, inFunc) {
        if (!this.observemap[inPrefix]) {
            return; // this.observermap[inPrefix] = new Object();
        }
        if (!this.observemap[inPrefix][inFeature]) {
            return; // this.observermap[inPrefix][inFeature] = new Array();
        }
        var flist = this.observemap[inPrefix][inFeature];
        var ic = flist.length;
        var iremove = -1;
        for(var i = 0; i < ic; i++) {
            if (flist[i].func == inFunc) {
                iremove = i;
                break;
            }
        }

        if (iremove > -1) {
            flist.splice(iremove,1);
        }

    } // unregister_observer

    // debug support
    this.dump_observemap = function() {
        var s = '';
        for(var p in this.observemap) {
            s += 'observemap['+ p +']';
            for(var f in this.observemap[p]) {
                var ic = f.length;
                s += '[' + f + ']';
                for(var i = 0; i < ic; i++) {
                    s += '['+i+'].args = '+ to_string_shallow(this.observemap[p][f][i].args);
                }
            }
        }
        return s;
    } // dump_observermap
} // bbtool.observable_mixin

// link up the fvlogger into current class
// usage: bbtool.debug_mixin.apply(this, []);
window.bbtool.debug_mixin = function() {
    // No op
    this.noop = function() { return true; }

    // Debugging output
    this.dsr_count = 0;
    if (window.debug) { // use fvlogger
        this.debug = window.debug;
    } else if (top.debug) {
        this.debug = top.debug;
    } else {
        // no debugging
        this.debug = this.noop();
    }

    if (window.warn) { // use fvlogger
        this.warn = window.warn;
    } else if (top.warn) {
        this.warn = top.warn;
    } else {
        // no debugging
        this.warn = this.noop();
    }

    if (window.info) { // use fvlogger
        this.info = window.info;
    } else if (top.info) {
        this.info = top.info;
    } else {
        // no debugging
        this.info = this.noop();
    }

    if (window.error) { // use fvlogger
        this.error = window.error;
    } else if (top.error) {
        this.error = top.error;
    } else {
        // no debugging
        this.error = this.noop();
    }
} // bbtool.debug_mixin


// factory for creating dialogs.
// All dialogs are appended after inTopElem.lastChild
// dialogs are initially invisible and widgetize()d.
// There should exists a bb.bbclassname class that isa() bbdhtml_dialog
//   which will control the dialog.
// params: bbclassname, css_class, title, title_menu_left, title_menu_right
bbtool.dialog_factory = function(inParams) {

    // this.dialog_root = inTopElem;

    this.cfg = new Object();
    this.cfg.bbclassname = 'bbdhtml_dialog';
    this.cfg.cssclassname = 'bbbase_dialog';
    this.cfg.title = 'Base Dialog';

    // ### must functions to call and labels for the functions
    this.cfg.title_menu_left = 'TBD';
    // this.cfg.title_menu_right = "<a href='#' onclick='return hide_window(\"" + this.cfg.dialog_id + "\");'>Close</a>";
    this.cfg.dialog_id = 'base_dialog';

    // override any of the above
    if (inParams) {
        var k = 0;
        for(k in inParams) {
            this.cfg[k] = inParams[k];
        }
    }

    // Private
    // The standard dialog decorations
    this._get_innerhtml = function(inParam) {
        var html = "";
        // title bar
        // html += "<div id='" + this.bbclassname + "' ";
        //html += " class='" + this.bbclassname + " " + this.cssclassname + " bbinvisible'>";
        if (0) {
            html += "<table class='title_bar'><tr>";
            html += "<td><span class='left_menu'>"+inParam.title_menu_left+"</span></td>";
            html += "<td><span class='title'>"+inParam.title+"</span></td>";
            html += "<td><span class='right_menu'>"+inParam.title_menu_right+"</span></td>";
            html += "</table>";
        } else {
            html += "<div class='title_bar'>";
            // <!-- title bar expected to contain left_buttons, title, right buttons -->
            html += "<span class='left_menu'>"+inParam.title_menu_left+"</span>";
            html += "<span class='right_menu'>"+inParam.title_menu_right+"</span>";
            html += "<span class='title'>"+inParam.title +"</span>";
            html += "</div>";
        }
        // extra menu bar
        html += "<div class='tool_bar'>";
        html += "</div>";

        // content area
        html += "<div class='content'>";
        html += "</div>";

        // html += "</div>";
        return html;
    } // _get_innerhtml

    // Private
    // create the <div> that becomes the dialog
    this._init_dialog = function(inParam) {
        var dialog = document.createElement('div');
        // dialog.setAttribute('id', inParam.bbclassname);
        dialog.setAttribute('id', inParam.dialog_id);
        dialog.setAttribute('class',  (inParam.bbclassname + " " + inParam.cssclassname + " bbbase_dialog bbinvisible") );
        dialog.innerHTML = this._get_innerhtml(inParam);
        return dialog;
    } // init_dialog

    // API
    // actually make and install a new kind of dialog
    this.new_dialog = function(inParam) {
        var defaults = clone(this.cfg);
        // override any of the above
        if (inParam) {
            var k = 0;
            for(k in inParam) {
                defaults[k] = inParam[k];
            }
        }
        if (!inParam.dialog_id) {
            defaults.dialog_id = defaults.bbclassname;
        }
        if (!inParam.title_menu_left) {
            defaults.title_menu_left = 'TBD';
        }
        if (!inParam.title_menu_right) {
            defaults.title_menu_right = "<a href='#' onclick='return hide_window(\"" + defaults.dialog_id + "\");'>Close</a>";
        }

        window.dispatcher.debug(to_string_shallow(this));

        var dlg = this._init_dialog(defaults);

        // var b = document.getElementsByTagName('body');
        // b[0].insertBefore(dlg, null);
        var dynamic_dialog_container = document.getElementById('bbdynamic_dialog_container');
        if (!dynamic_dialog_container) {
            window.dispatcher.error("No dialog container found: 'bbdynamic_dialog_container'");
        }
        dynamic_dialog_container.insertBefore(dlg, null);

        widgetize(dlg.parentNode);
        return dlg;
    } // new_dialog

    // API
    // return list of existing dialog elements
    this.get_all_dialogs = function() {
        var dynamic_dialog_container = document.getElementById('bbdynamic_dialog_container');
        var result = false;
        if (!dynamic_dialog_container) {
            window.dispatcher.error("No dialog container found: 'bbdynamic_dialog_container'");
        } else {
            result = dynamic_dialog_container.childNodes;
        }
        return result;
    } // get_all_dialogs

    // API
    this.destroy_dialog = function(inDialogElem) {
        this.dialog_root.removeChild(inDialogElem);
    }

} // bbtool.dialog_factory
bbtool.dialog = new bbtool.dialog_factory(document.body);

// Provides collection of observers implementing common dialog behaviors
// on the 'content area'.
// Arrow key navigation, Enter-key->selection, on click selection, Escape->cancel
// Should vastly simplify customizing the behavior of dialogs from bbtool.dialog.new_dialog()
window.bbtool.dialog_content_manager = function(inParam) {
    bb.bbobject.apply(this);
    this.add_isa('dialog_content_manager');

    // inParam members
    //this.col_tag = 'td';
    //this.row_tag = 'tr';
    //this.dialog_elem = null;
    //this.dialog_return_field_name = 'dialog_id';


    // Event Keystroke enter
    // taken from browser_base.js bbinventory_dialog.save_user_selection()
    this.handle_key_enter = function (inElement, inArgs, inEvent) {
        var w = inElement.widget;
        var me = inArgs;
        w.debug(me.get_dialog_id() + ': handle_key_enter');

        var lis = w.content.getElementsByTagName(me.row_tag);
        if (lis.length > 1) {
            // we actually have some results
            var sel = lis[w.arrow.row_current];
            var item_number = getElementMatchingName(sel, me.dialog_return_field_name);
            if (item_number) {
                // w.save_user_selection(w.line_item, item_number.getValue(), sel);
                me.return_field.focus();
                me.return_field.setValue(item_number.getValue());
                me.return_field.blur();
                if ((geckoGetRv() >= 1.080100))  {
                    // HACK: in FF < 2.0 inElement.setValue followed by inElement.blur() triggered onchange
                    // in FF 2.0 onchange not triggered
                    // known versions
                    // Market version   Gecko    geckoGetRv
                    // ------------------------------------
                    // FF 1.5.0.4,      1.8.0.4, 1.080004
                    // Seamonkey 1.1.3, 1.8.1.5, 1.080105
                    // FF 2.0.0.4,      1.8.1.4, 1.080104
                    // FF 3.0.6         1.9.0.6  1.090006
                    w.error('handle_key_enter: Gecko/1.8.1 force change event  '+me.return_field.name);
                    window.dispatcher.do_change(me.return_field);
                }
            }
        }

        me._close_dialog_elem();
        return true;
    } // handle_key_enter

    // Event Keystroke escape
    // close dialog with no changes to data
    this.handle_key_escape = function (inElement, inArgs, inEvent) {
        // this is the dialog_elem.widget
        var w = inElement.widget;
        var me = inArgs;
        w.debug(me.get_dialog_id() + ': handle_key_escape');
        me._close_dialog_elem();
        return true;
    } // handle_key_escape

    // Private
    // support handle_pre_show
    // save currently focused bbfield that will receive the result of the dialog.
    this._save_return_row = function() {
        // get targeted row
        var ok = true;
        var field;
        var inputlist = false;
        var rt;
        if (window.dispatcher.focus_row) {
            // insert into currently focused row, item number field
            rt = window.dispatcher.focus_row;
            field = this.return_field_name + ']';
            inputlist = getElementMatchingName(rt, field);
            // if no focused row, find insert row
        }
        // var item = inputlist;
        if (inputlist) {
            this.return_field = inputlist;
            this.return_row = getAncestorByClass(inputlist, 'bbrecord');
            this.restore_focus = new bbtool.focus_hold(this.return_field);
        }
    } // dialog_content_manager._save_return_row

    // Private
    // close dialog
    this._close_dialog_elem = function () {
        var w = this.dialog_elem.widget;
        w.hide();
        w.clear_content();
        if (this.restore_focus) {
            this.restore_focus.focus(); // should be in w.hide()
        }
        this.restore_focus = null;
    } // _close_dialog_elem

    // Observe dialog_elem.pre_show
    // install event handlers for common keys when dialog visible
    this.handle_pre_show = function (inPrefix, inFeature, inArgs) {
        // this == dialog_elem.widget, inArgs is the dialog
        // controller-- subclass of dialog_content_manager
        var me = inArgs;
        var w = this;
        // ### hazardous subclasses will want different behavour on enter
        window.keymap.register_key(key.Enter, w.elem , me.handle_key_enter, me, me.get_dialog_id() + ' check enter');
        window.keymap.register_key(key.Esc, w.elem , me.handle_key_escape, me, me.get_dialog_id() + ' esc_close');
        if (w.arrow) {
            w.arrow.register();
        }
        me._save_return_row();

        return w.event_proceed;
    } // dialog_content_manager.handle_pre_show

    // Observe dialog_elem.pre_hide
    // remove event handlers for common keys.
    this.handle_pre_hide = function (inPrefix, inFeature, inArgs) {
        // this == dialog_elem.widget, inArgs is dialog_content_manager
        var me = inArgs;
        var w = this;
        window.keymap.unregister_key(key.Enter, w.elem);
        window.keymap.unregister_key(key.Esc, w.elem);
        if (w.arrow) {
            w.arrow.unregister();
        }
        return w.event_proceed;
    } // dialog_content_manager.handle_pre_hide

    // Observe dialog_elem.post_hide
    // remove observers.  this is the last observer called for our class.
    this.handle_post_hide = function(inPrefix, inFeature, inArgs) {
        // this == dialog_elem.widget, inArgs is dialog_content_manager
        var me = inArgs;
        var w = this;
        w.unregister_observer(me.get_prefix(), 'pre_show', me.handle_pre_show);
        w.unregister_observer(me.get_prefix(), 'pre_hide', me.handle_pre_hide);
        w.unregister_observer(me.get_prefix(), 'post_connect_to_content', me.handle_post_connect_to_content);
        // bwm broken w.unregister_observer(me.get_prefix(), 'pre_connect_to_content', me.handle_post_connect_to_content);
        w.unregister_observer(me.get_prefix(), 'post_hide', me.handle_post_hide); // ### self modification
    } // dialog_content_manager.handle_post_hide

    // Observe dialog_elem.post_connect_to_content
    // move cursor/selection to first item in content.
    this.handle_post_connect_to_content = function (inPrefix, inFeature, inArgs) {
        // this == dialog_elem.widget, inArgs is dialog_content_manager
        var me = inArgs;
        var w = this;
        var rows = w.content.getElementsByTagName(me.row_tag);
        if (rows.length > 0) {
            w.content.focus();
            w.arrow.goto_first_row();
        }
        return w.event_proceed;
    } // dialog_content_manager.handle_post_connect_to_content

    this.get_dialog_id = function() {
        return this.dialog_elem.widget.get_dialog_id();
    } // get_dialog_id

    this.get_prefix = function() {
        return this.dialog_elem.widget.get_prefix();
    } // get_prefix

    //

    // reset
    this.initialize = function(inParam) {
        var ok = true;
        this.name = 'dialog_content_manager';
        if (!inParam.dialog_elem || !inParam.dialog_elem.widget.isa('bbdhtml_dialog')) {
            // dialog window element not given
            window.error('dialog window element not given as dialog_elem');
            ok = false;
        } else {
            this.dialog_elem = inParam.dialog_elem;
        }
        if (!inParam.col_tag) {
            this.col_tag = 'td';
        } else {
            this.col_tag = inParam.col_tag; // arrow key column tag, 'td'
        }

        if (!inParam.row_tag) {
            this.row_tag = 'tr';
        } else {
            this.row_tag = inParam.row_tag; // arrow key row tag, 'tr'
        }

        if (!inParam.return_field_name) {
            // name of bbrecord field to copy the selected row data into.
            window.error('invalid return_field_name param');
            ok = false;
        } else {
            this.return_field_name = inParam.return_field_name;
        }

        if (!inParam.dialog_return_field_name) {
            // name of dialog row field to copy from
            window.error('invalid dialog_return_field_name param');
            ok = false;
        } else {
            this.dialog_return_field_name = inParam.dialog_return_field_name;
        }

        if (ok) {
            var w = this.dialog_elem.widget;

            if (!w.arrow || !w.arrow.unregister) {
                w.arrow = new bbtool.arrow_key_manager(w.content,
                                                   { name:this.get_dialog_id() + ' Arrow',
                                                       col_tag:this.col_tag});
            }
            w.register_observer(this.get_prefix(), 'pre_show', this.handle_pre_show, this);
            w.register_observer(this.get_prefix(), 'pre_hide', this.handle_pre_hide, this);
            w.register_observer(this.get_prefix(), 'post_hide', this.handle_post_hide, this);
            w.register_observer(this.get_prefix(), 'post_connect_to_content', this.handle_post_connect_to_content, this);
        }
        this.dialog_elem.widget.dialog_manager = this;
        return ok;
    } // initialize

    this.initialize(inParam);
} // bbtool.dialog_content_manager;


bbtool.t_money = {

    precision: 1000000.0,
    money_unit: 0.000001,
    places: 6,         // working decimal places
    money_decimal: 2,  // display decimal places

    // API Public Static
    // do SQL equivilent rounding function
    // taken from
    // http://www.mediacollege.com/internet/javascript/number/round.html
    round:function (rnum, rlength) { // Arguments: number to round, number of decimal places
        if (!rlength) {
            rlength = bbtool.t_money.places;
        }
        var newnumber = Math.round(rnum*Math.pow(10,rlength))/Math.pow(10,rlength);
        return newnumber;
    }, // round

    // API Public Static
    // format number as 2 or more decimal places
    // cash is string representing number to format
    // Negative sign obeys usual math rules.
    // Currency sign omitted.
    // should match servcie/data/t_money.php
    money_print: function(inCash, inForcePlace) {
        var cash = String(inCash);
        var cp = cash.indexOf('.');
        if (cp === -1) {
            cash += '.';
            cp = cash.indexOf('.');
        }
        cash = cash.replace(/0*$/, '');
        var cl = cash.length-1;
        var dl = 2;
        if (!inForcePlace) {
            var dl = cl - cp;
            if (dl < 2) {
                dl = 2;
            }
            if (dl > 6) {
                dl = 6;
            }
        } else {
            dl = inForcePlace;
        }
        if (!cash || isNaN(cash)) {
            window.info('bad cash '+cash);
            cash = '0.00';
        }
        var out = Number(cash).toFixed(dl); // does math standard rounding
        // window.info('money_print: Converting ' + inCash + ' to ' + out);
        return out;
    } // money_print
} // bbtool.t_money

// window.info('money_print: '+window.bbtool.t_money.precision);


// do not instantiate directly, instead use
// bbtool.msgbox.show_message('my message', ['msgtype']);
window.bbtool._msgbox = function() {
    this.init_once = function() {
        this.msgbox_cfg = {
            bbclassname:'bbdhtml_dialog',
            cssclassname:'bbbase_dialog',
            title:'Message',
            title_menu_left:' ',
            dialog_id:'msgbox_dialog',
            prefix:'_msgbox'
        }

        this.msgbox = window.bbtool.dialog.new_dialog(this.msgbox_cfg);
        this.msgbox.widget.set_prefix(this.msgbox_cfg.prefix);
        this.name = this.msgbox.widget.get_prefix();

        this.control_cfg = {
            dialog_elem:this.msgbox,
            return_field_name:'msgbox_response',
            dialog_return_field_name:'dialog_msgbox_response'
        }

        // need hidden field dialog_msgbox_response to get user-answer
        this.controller = new bbtool.dialog_content_manager(this.control_cfg);
        this.is_open = false;
    } // init_once

    // API
    // show the message dialog box, non-modal
    // inMessage is content to display, inCfg is optional
    // alternateive dialog configuration
    this.show_message = function(inMessage, inCfg) {
        if (!this.controller) {
            this.init_once();
        }
        var w = this.msgbox.widget;
        var ok = true;
        if (this.is_open) {
            // append message to existing?
            // queue message?
            w.hide();
            w.unregister_observer(this.controller.get_prefix(),
                                  'pre_show', this.handle_pre_show);
            w.unregister_observer(this.controller.get_prefix(),
                                  'pre_hide', this.handle_pre_hide);
            w.unregister_observer(this.controller.get_prefix(),
                                  'pre_show', this.handle_content);
        }

        if (ok) {
            if (inCfg) {
                // destroy, reconstruct this.msgbox,
                // fixup this.control_cfg.dialog_elem = this.msgbox
            }
            this.hold_message = inMessage;

            this.controller.initialize(this.control_cfg);
            w.register_observer
                      (this.controller.get_prefix(),
                       'pre_show', this.handle_pre_show, this);
            w.register_observer
                      (this.controller.get_prefix(),
                       'pre_hide', this.handle_pre_hide, this);
            w.register_observer
                      (this.controller.get_prefix(),
                       'pre_show', this.handle_content, this);
        }

        if (ok) {
            w.show();
        }
    } // show_message

    //Private
    // record whether window opens successfully
    this.handle_pre_show = function(inPrefix, inFeature, inArgs) {
        // fix this
        return inArgs._msgbox_handle_pre_show(inPrefix, inFeature);
    }
    this._msgbox_handle_pre_show = function(inPrefix, inFeature) {
        this.is_open = true;
        return this.msgbox.widget.event_proceed;
    }

    //Private
    // record whether window closes successfully
    this.handle_pre_hide = function(inPrefix, inFeature, inArgs) {
        // fix this
        return inArgs._msgbox_handle_pre_hide(inPrefix, inFeature);
    }
    this._msgbox_handle_pre_hide = function(inPrefix, inFeature) {
        this.is_open = false;
        return this.msgbox.widget.event_proceed;
    }

    //Private
    // actually put the message into the content
    this.handle_content = function(inPrefix, inFeature, inArgs) {
        // fix this
        return inArgs._msgbox_handle_content(inPrefix, inFeature);
    }
    this._msgbox_handle_content = function(inPrefix, inFeature) {
        var w = this.msgbox.widget;
        w.content.innerHTML = "<div class='msgbox'>" +
                  this.hold_message +
                  "<"+"/div>";

        return this.msgbox.widget.event_proceed;
    }
} // bbtool.bbmsgbox

window.bbtool.prefix_to_array = function(inPrefix) {
    if (inPrefix) {
        var parts = inPrefix.split('[');
    }
    var ic = parts.length;
    for(var i = 0; i < ic; i++) {
        if (parts[i].charAt( parts[i].length - 1 ) == ']') {
            parts[i] = parts[i].substr(0, parts[i].length - 1);
        }
    }
    return parts;
}

window.bbtool.msgbox = new window.bbtool._msgbox();


Array.prototype.index_of = function(v) {
    if (this.indexOf) {
        return this.indexOf(v);
    } else {
        var found = -1;
        var jc = this.length;
        for(var j = 0; j < jc; j++) {
            if (v == this[j]){
                found = j;
                break;
            }
        }
        return found;
    }
} // index_of
