/* -*- 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

*/
var ns4 = document.layers;
var ie4 = document.all;
var nn6 = document.getElementById && !document.all;

// constructor
// dispatcher/registry
function dispatcher() {
    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);
            }
            this.info('send_event(' + inEventName + ',' + elem.name + ')');
            elem.dispatchEvent(ne);
            this.info('Direct exec (' + inEventName + ',' + elem.name + ')');
            this.list_dispatch.apply(elem, [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
    // 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() {
                window.dispatcher.debug("force "+this.wait_id+" invoke on "+(inEventObj?inEventObj.target.tagName:'')+ " with " + to_string(this.arglist) );
                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;
            }
        }
    }

    // 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 (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();
        }

        this.scan = function() {
            window.dispatcher.serialized.do_serialized();
            setTimeout(window.dispatcher.serialized.scan, 1000);
        }
    };

    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() {
                window.dispatcher.debug("force "+this.wait_id+" invoke on " + (inEventObj?inEventObj.target.tagName:'')+ " with " + to_string(this.arglist) );
                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,
    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.debug('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.

            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+"'");
        }
    } // dispatcher.p_register

    // Remove an event handler
    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+"'");
        }
    } // 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+'"');
        }
    }

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

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

} // dispatcher


window.bb = new Object();

// 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
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', 'toolbar');
    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;
        }
    }

    this.get_prefix = function() {
        return this.prefix;
    }

    // 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;
        }
        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 old_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;
    } // bbchoose_dialog.show


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

    this.load_html_toolbar = function(inHtml) {
        if (ok) {
            var evt = this._apply_feature(this.prefix, 'pre_load_html_toolbar', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }

        this.toolbar.innerHTML = inHTML;

        if (ok) {
            var evt = this._apply_feature(this.prefix, 'post_load_html_toolbar', this);
            if (evt != this.event_proceed) {
                ok = false;
                return evt;
            }
        }
    }
    // 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>
         * <convert to DOM>
         * post_load
         */
        var evt = this._apply_feature(this.prefix, 'pre_load_json_content', this);
        if (evt != this.event_proceed) {
            return evt;
        }

        var otxt = JSON.parse(inResponseText);
        var ok = (otxt instanceof Object);
        if (ok) {
            this.debug("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.
                 */
            } else {
                ok = false;
                this.error('Incorrect dialog id received: '+otxt['receive']+' expecting '+w.get_dialog_id());
            }
        }
        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) {
            temp_content = this.content.innerHTML;
            if (this.rs.records.length > 0) {

                var rt = new bbtool.result_table(this.rs.records, this.rs._columns);
                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
    }

    // 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;
        }
    }

    // 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;
        }
        widgetize(this.content);

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


} //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.debug('bbchoose_dialog.erase()');
        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);
            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.bbinventory_dialog = function(inWidgetElem, inModelID) {
    bb.bbchoose_dialog.apply(this, [inWidgetElem, inModelID]); // extends bbchoose_dialog
    this.add_isa('bbinventory_dialog');

    // Difference vs choose dialog:
    // can redo search.
    // next/prev items.
    this.line_item = null;
    this.line_row = null;
    this.line_ship_pt = null;
    this.line_qty_ordered = null;
    this.line_qty_shipped = null;
    this.dialog_id = 'req_line_inventory_dialog';
    this.restore_focus = null;

    this.query_result = new Array();
    this.query_column = new Array();

    this.bbchoose_dialog_hide = this.hide;
    this.hide = function() {
        this.bbchoose_dialog_hide();
        if (this.restore_focus) {
            this.restore_focus.focus();
            this.restore_focus = null;
        }
        window.keymap.unregister_key(key.Enter, this.elem);
        window.keymap.unregister_key(key.Esc, this.elem);
        if (this.arrow) {
            //this.debug('this.arrow is '+to_string_shallow(this.arrow));
            this.arrow.unregister();
        }
    }
    this.bbchoose_dialog_show = this.show;
    this.show = function() {
        // get targeted row
        var ok = true;
        var field;
        var inputlist = false;
        var rt;
        if (window.dispatcher.focus_row) {
            // insert into currently focused row
            rt = window.dispatcher.focus_row;
            field = 'req_lineitem_number]';
            inputlist = getElementMatchingName(rt, field);
        }
        if (!inputlist) {
            // not focused on a req_line, target insert row
            rt = document.getElementById('dg');
            field = 'insert][req_lineitem_number]';
            inputlist = getElementMatchingName(rt, field);
            if (!inputlist) {
                window.dispatcher.error('cannot find req_line insert row');
                ok = false;
            }
        }

        var item = inputlist;
        if (ok) {
            this.line_item = item;
            this.line_row = getAncestorByClass(item, 'bbrecord');
            this.line_qty_ordered = getElementMatchingName(this.line_row, 'req_lineqty_ordered');
            this.line_description = getElementMatchingName(this.line_row, 'req_linedescription');
            //this.line_qty_shipped = getElementMatchingName(targ, 'req_linequantity');
            //this.line_ship_pt = getElementMatchingName(targ, 'req_lineship_pt');
            this.restore_focus = new bbtool.focus_hold(this.line_description);
        }

        if (ok) {
            ok = this.bbchoose_dialog_show();
            keymap.register_key(key.Enter, this.elem ,this.check_enter, {label:this.dialog_id + ' check enter'});
            keymap.register_key(key.Esc, this.elem ,this.esc_close, this, this.dialog_id + ' esc_close');
            if (this.arrow) {
                this.debug('this.arrow is '+to_string_shallow(this.arrow));
                this.arrow.register();
            }
            // focus the search_term control
            var ctrl = document.getElementsByName('search_term');
            if (ctrl.length) {
                ctrl[0].focus();
            } else {
                this.error("Cannot find search_term_elem");
            }
        }
        return ok;
    } // bbinventory_dialog.show

    // Event escape key
    this.esc_close = function(inElement, inArgs, inEvent) {
        var w = inArgs;
        w.erase();
        w.hide();
    }

    // Event
    // handle enter key
    // if focus on search field then do search.
    // if focus on content then do save and close on hilighted row
    this.check_enter = function(inElement, inArgs, inEvent) {
        w = inElement.widget;
        window.dispatcher.debug('check_enter ');
        if (w.search_term.has_focus == true) {
            window.dispatcher.debug('search_term ');
            // w.send_search(inElement, {label:w.dialog_id}, inEvent);
        } else if (w.search_type.has_focus == true) {
            window.dispatcher.debug('search_type ');
            // w.send_search(inElement, {label:w.dialog_id}, inEvent);
        } else if (w.content.has_focus == true) {
            window.dispatcher.debug('content ');
            var lis = w.content.getElementsByTagName('tr');
            if (lis.length > 1) {
                // we actually have some results
                var sel = lis[w.arrow.row_current];
                var item_number = getElementMatchingName(sel, 'hold_item_number');
                if (item_number) {
                    w.save_user_selection(w.line_item, item_number.getValue());
                    w.erase();
                }

            } else {
                w.send_search(inElement, {label:w.dialog_id}, inEvent);
            }
        }
    } // bbinventory_dialog.check_enter


    this.save_user_selection = function(inElement, inValue) {
        this.debug('save_user_selection: focusing '+inElement.name);
        inElement.focus();
        this.debug('save_user_selection: setting '+inElement.name);
        inElement.setValue(inValue);
        this.debug('save_user_selection: bluring '+inElement.name);
        inElement.blur();
        if (navigator.userAgent.indexOf('Firefox/2') > -1) {
            // HACK: in FF < 2.0 inElement.setValue followed by inElement.blur() triggered onchange
            // in FF 2.0 onchange not triggered
            this.error('save_user_selection: Firefox/2 force change event  '+inElement.name);
            window.dispatcher.do_change(inElement);
        }
    } // bbinventory_dialog.save_user_selection

    this.load_result_pre_display = function(inData) {
        // to easily find the part number later
        if (this.query_column && this.query_column.length > 0) {
            for(var i = 0; i < this.query_result.length; i++) {
                this.query_result[i]['hold_item_number'] = this.query_result[i]['part_number'];
                var desc = this.query_result[i]['name'];
                if (desc && (desc.length > 40)) {
                    this.query_result[i]['name'] = desc.substring(0,40) + '...';
                }
            }
            this.query_column.push({
                                    label:'',
                                    short_label:'',
                                    field_name:'hold_item_number',
                                    alias_name:'hold_item_number',
                                    format:'input_hidden'
                               });


        }
    } // load_result_pre_display

    // async send search terms to server
    this.send_search = function(inElement, inArgs, inEvent) {
        var w = to_element('inventory_dialog').widget;
        var ok = true;
        if (!w.search_type) {
            w.error("No search_type field given");
            ok = false;
        }
        if (!w.search_term) {
            w.error("No search_term field given");
            ok = false;
        }
        if (ok) {
            rtn_elem = w.line_item;
            if (rtn_elem) {
                var head_key = document.getElementsByName('head_key')[0];
                var head_table = document.getElementsByName('head_table')[0];
                var args = {
                    view:'inventory_aj_list',
                    action:'inventory_aj_find',
                    search_type:w.search_type.getValue(),
                    search_term:w.search_term.getValue(),
                    receive:w.dialog_id,
                    use_table:w.dialog_id,
                    head_key:head_key.getValue(),
                    head_table:head_table.getValue(),
                    part_prefix:'',
                    display_mode:'json'
                }

                w.clear_content();
                var cnode = document.createElement('p');
                cnode.innerHTML='...';
                w.content.appendChild(cnode);
                w.dispatcher.debug('send_search sending (' + to_string(args) + ')' );
                w.dispatcher.dispatch_aj(inEvent.type, w.elem,
                                    background_send,
                                    w.receive_search, args, 'Inventory Search');
                // background_send(rtn_elem, args, inResponseCB, inEvent);
            } else {
                w.error('return element "insert][req_lineitem_number]" not found');
            }
        }

    } // bbinventory_dialog.send_search

    // Async receive search results from server
    this.receive_search = function(inReq, inArgs, inElement, inSuccess) {
        var w = inElement.widget;
        debug('receive_search(' + to_string(inArgs) + ')' );
        var ok = inSuccess;
        if (ok) {
            var stat = inReq.status;
            ok = (stat == 200);
        } else {
            w.warn("send_search() failed");
        }
        var otxt = null;
        if (ok) {
            var jtxt = inReq.responseText;
            debug_server_reply(inReq, inArgs, inElement, inSuccess);
            otxt = JSON.parse(jtxt);
            ok = (otxt instanceof Object);
        }
        if (ok) {
            w.debug("otxt.receive: '"+otxt.receive+"'");
            if (otxt['receive'] == w.dialog_id) {
                w.clear_content();
                w.load_picks(otxt, inElement);
                w.connect_to_result();
            } else {
                // should not happen
                w.error('wrong dialog response received:' + otxt['receive'] + ' expecting ' + w.dialog_id);
            }
        } else {
            warn("cannot decode response: " + jtxt);
        }
    } // bbinventory_dialog.receive_search

    // install handlers on this.content.inputs, etc
    this.connect_to_result = function() {
        var rows = this.content.getElementsByTagName('tr');
        this.debug('connect_to_result found '+rows.length+' <tr>');
        if (this.arrow) {
            //this.arrow.register();
        } else {
            this.error('arrow manager not found');
        }

        if (rows.length > 0) {
            var parts = 0;
            for(var i = 0; i < rows.length; i++){
                var pn = rows[i].getElementsByTagName('input');
                if (pn && pn.length > 0) {
                    j =0;
                    while ((j < pn.length) && (pn[j].name.indexOf('hold_item_number') == -1)) {
                        j++;
                    }
                    if (pn[j]) {
                        this.set_onclick(rows[i], this.line_item, pn[j].getValue());
                    }
                }
            }
            this.content.focus();
            this.arrow.goto_first_row();
        } else {
            // no results, try again
            this.search_term.focus();
        }
    } // connect_to_result()

    // Event onFocus, onBlur
    // assign has_focus parameter to the currently focused component
    // of this dialog
    this.track_has_focus = function(inElement, inArgs, inEvent) {
        var dlg = to_element('inventory_dialog');
        switch(inEvent.type) {
        case 'blur': inElement.has_focus = false; break;
        case 'focus': inElement.has_focus = true; dlg.widget.current_focus = inElement; break;
        default: window.dispatcher.error('track_current_focus: invalid event type '+inEvent.type);
        }
    }

    // find <form>  <input> and attach triggers
    // may be generalizable. somehow
    this.connect_to_view = function () {
        var inputs = getElementsByTagNames('input,select,textarea', this.elem);

        var il = inputs.length;
        if (!this.arrow || !this.arrow.unregister) {
            this.arrow = new bbtool.arrow_key_manager(this.content,
                                                  { name:this.name + ' Arrow',
                                                      col_tag:'td' });
        }

        var i = 0;
        for(i = 0; i < il; i++) {
            if (inputs[i].name == 'search_term') {
                this.search_term = inputs[i];
                this.info('found search term field');
            }
            if (inputs[i].name == 'search_type') {
                this.search_type = inputs[i];
                this.info('found search type field');
            }
            // onchange--> do search, install results
        }
        var args = {
            widget: this,
            search_type: this.search_type,
            search_term: this.search_term
        }

        this.dispatcher.register('change', this.search_term,
                                 this.send_search,
                                 this, 'change_search_term_send_search');
        this.dispatcher.register('focus', this.search_term,
                                 this.track_has_focus,
                                 this, 'focus_search_term_track_has_focus');
        this.dispatcher.register('blur', this.search_term,
                                 this.track_has_focus,
                                 this, 'blur_search_term_track_has_focus');

        this.dispatcher.register('focus', this.search_type,
                                 this.track_has_focus,
                                 this, 'focus_search_type_track_has_focus');
        this.dispatcher.register('blur', this.search_type,
                                 this.track_has_focus,
                                 this, 'blur_search_type_track_has_focus');

        this.dispatcher.register('focus', this.content,
                                 this.track_has_focus,
                                 this, 'focus_content_track_has_focus');
        this.dispatcher.register('blur', this.content,
                                 this.track_has_focus,
                                 this, 'blur_content_track_has_focus');
    } // connect_to_view

    this.connect_to_view();
}  // bbinventory_dialog


bb.bbship_pt_dialog = function(inWidgetElem, inModelID) {
    bb.bbchoose_dialog.apply(this, [inWidgetElem, inModelID]); // extends bbchoose_dialog
    this.add_isa('bbship_pt_dialog');
    this.name = 'Ship From Dialog';

    // Difference vs choose dialog:
    // allows user input beyond merely picking something.

    this.line_item = null;
    this.line_row = null;
    this.line_ship_pt = null;
    this.line_qty_ordered = null;
    this.line_qty_shipped = null;
    this.dialog_id = 'req_line_ship_pt_dialog';
    this.restore_focus = null;

    this.query_result = new Array();
    this.query_column = new Array();
    this.arrow = new bbtool.arrow_key_manager(this.content, {name:this.name + ' Arrow'});

    this.bbchoose_dialog_hide = this.hide;
    this.hide = function() {
        this.bbchoose_dialog_hide();
        keymap.unregister_key(key.Enter, this.elem);
        keymap.unregister_key(key.Esc, this.elem);
        if (this.arrow) {
           this.arrow.unregister();
        }
    }

    this.bbchoose_dialog_show = this.show;
    this.show = function() {
        // find the item_number or fail
        // row of an existing item must be focused
        var ok = true;
        var targ = null;
        if (window.dispatcher.focus_row) {
            targ = window.dispatcher.focus_row;
        }

        var item = null;
        if (targ && getAncestorByClass(targ, 'bbdatagrid')) {
            // we are on a dg row;
            item = getElementMatchingName(targ, 'req_lineitem_number');
        } else {
            this.error('not in bbdatagrid: '+targ.tagName);
            ok = false;
        }

        // exclude [insert] row
        var insert_row = new RegExp('\\[insert]');
        if (insert_row.test(item.name)) {
            this.error('ship_pt dialog not allowed on insert row');
            ok = false;
        }

        var ret = null;
        if (item) {
            // good
            this.line_item = item;
            this.line_row = targ;
            this.line_qty_ordered = getElementMatchingName(targ, 'req_lineqty_ordered');
            this.line_qty_shipped = getElementMatchingName(targ, 'req_linequantity');
            this.line_ship_pt = getElementMatchingName(targ, 'req_lineship_pt');
            this.restore_focus = new bbtool.focus_hold(this.line_qty_ordered);
        } else {
            this.error('not focused on a req_line');
            ok = false;
        }

        if (ok) {
            this.bbchoose_dialog_show();
            this.do_get_ship_pts();
            window.keymap.register_key(key.Enter, this.elem ,this.save_close, {label:this.dialog_id});
            window.keymap.register_key(key.Esc, this.elem ,this.esc_close, this, {label:this.dialog_id});

            if (this.arrow) {
                this.arrow.register();

            }
        }
        return ok;
    } // bbship_pt_dialog.show


    // start the load shipping points event sequence
    // send_search -> receive_search -> update display
    this.do_get_ship_pts = function() {
        this.debug('do_get_ship_pts;');
        var head_key = document.getElementsByName('head_key')[0];
        var head_table = document.getElementsByName('head_table')[0];
        var args = {
            action:'location_aj_find_ship_pt',
            view:'location_aj_list',
            search_term:this.line_item.getValue(),
            receive:this.dialog_id,
            use_table:this.dialog_id,
            head_key:head_key.getValue(),
            head_table:head_table.getValue(),
            part_prefix:'',
            display_mode:'json'
        };
        this.clear_content();
        var cnode = document.createElement('p');
        cnode.innerHTML='...';
        this.content.appendChild(cnode);
        this.dispatcher.dispatch_aj('change', this.elem,
                                    background_send,
                                    this.receive_ship_pts, args, 'Load Shipping Points');

    } // bbship_pt_dialog.do_get_ship_pts

    // Async receive search results from server
    // called in context of inElement not the widget element
    this.receive_ship_pts = function(inReq, inArgs, inElement, inSuccess) {
        var w = inElement.widget;
        w.debug('receive_ship_pts(' + to_string(inArgs) + ')' );
        var ok = inSuccess;
        if (ok) {
            var stat = inReq.status;
            ok = (stat == 200);
        } else {
            w.error("Load Shipping Points failed");
            ok = false;
        }
        var otxt = null;
        if (ok) {
            var jtxt = inReq.responseText;
            debug_server_reply(inReq, inArgs, inElement, inSuccess);
            otxt = JSON.parse(jtxt);
            ok = (otxt instanceof Object);
        }
        if (ok) {
            w.debug("otxt.receive: '"+otxt.receive+"'");
            if (otxt['receive'] == w.dialog_id) {
                while (w.content.firstChild) {
                    w.content.removeChild(w.content.firstChild);
                }
                w.load_picks(otxt, inElement);
            } else {
                // not meant for us
                w.error('Invalid response for Load Shipping Points');
                w.info('Invalid response was "'+to_string(otxt)+'"');
            }
        } else {
            w.error("cannot decode response: " + jtxt);
        }
    } //  bbship_pt_dialog.receive_ship_pts


    this.erase = function() {
        this.debug('bbship_pt_dialog.erase()');
        this.clear_content();
        window.choose_dialog_active = false;
    } // bbship_pt_dialog.erase

    // 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
         *  'pick_field' => field to return on selection
         *  'table' => name of this result set
         *  'context' => disposition of result set.
         */

        /*
         * whats missing:
         * must associate records/fields with data so
         * JS can get at all of it.
         * ability to re-query the data.
         */
        // 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;

        // add qty_wanted column, calc total qty,
        var resultlen = w.query_result.length;
        var columnlen = w.query_column.length;
        var total_available = 0;
        var total_ship_pts = 0;
        for(var i = 0; i < resultlen; i++) {
            w.query_result[i]['qty_wanted'] = 0;
            total_available = total_available + Number(w.query_result[i]['quantity']);
            total_ship_pts += 1;
        }
        // ### this should be a server-side calculated field.
        w.query_column.unshift({
                          label:'Wanted',
                          short_label:'Wanted',
                          field_name:'qty_wanted',
                          alias_name:'locationqty_wanted',
                          format:'input_text'
                      });

        var temp_content = '';
        if ((total_available + total_ship_pts) > 0)  {
            var rs = new bbtool.result_table(w.query_result, w.query_column,
                                         {name:'ship_pt_result'});
            temp_content += rs.get_html();
            // rs.add_line(...)
            temp_content += "<p>"+total_available+" Available</p>";
            temp_content += "<input type='button' name='ok' onclick=' "
                      + 'return to_element("ship_pt_dialog").widget.save_close(to_element("ship_pt_dialog"));'
                      + "' value='OK' >";
        } else  {
            temp_content += "<p>None Available</p>";
            temp_content += "<input type='button' name='ok' onclick=' "
                      + 'return hide_window("ship_pt_dialog");'
                      + "' value='OK' >";
        }
        w.content.innerHTML = temp_content;
        window.choose_dialog_active = true;

        // put focus in dialog
        this.connect_to_result();
    }

    // load ship_pt_dialog with inPicks.
    // inPicks is object with members
    //  'records' => result set of data for choose_dialog
    //  'results_field' => fieldname whose value to return as the pick item
    //  'show_fields' => array of field names [field1, field2, ...] in results to display (results[0][field1])
    //  'pick_field' => ???
    this.load_picks = function(inPicks) {
        return this.load_result(inPicks);
    } //  bbship_pt_dialog.load_picks

    // Event escape key
    this.esc_close = function(inElement, inArgs, inEvent) {
        var w = inArgs;
        w.debug(w.dialog_id + ': esc_close');
        w.hide();
        w.erase();
        w.restore_focus.focus(); // should be in w.hide()
        w.restore_focus = null;
    }

    // install handlers on this.content.<whatever>, initialize dialog
    // UI
    this.connect_to_result = function() {
        // window.dispatcher.dispatch('focus', this.elem, function () {}
        // var inputs = w.content.getElementsByTagName('input');
        // if (inputs.length ) { inputs[0].focus(); }
        var rows = this.content.getElementsByTagName('tr');
        if (rows.length > 0) {
            this.content.focus();
            this.arrow.goto_first_row();
        }
    }

    // handle the 'ok' button/enter key
    // save user selection and qty to the current_row.
    // if user made multiple selections, create new rows.
    this.save_close = function (inDlgElement) {
        var w = inDlgElement.widget;

        // get all qtys
        // if more than one qty  > 0, add line
        var rows = inDlgElement.getElementsByTagName('tr');
        var i = 0;
        var len = rows.length;
        var result_idx = 0;
        var result_len = w.query_result.length;
        var ship_from_id = null;
        var ship_from_label = null;
        var qty_wanted = null;
        var qty_wanted_field = null;
        var ship_pt_field = 0;
        var qty_ordered_field = 0;
        var current_line = w.line_row;
        var lines_filled = 0;
        for(i = 0; i < len; i++) {
            qty_wanted_field = getElementMatchingName(rows[i], 'qty_wanted');
            if (!qty_wanted_field) {
                w.info('skipping row #'+i+' with no qty_wanted');
                continue;
            }

            qty_wanted = qty_wanted_field.getValue();
            if (qty_wanted > 0) {
                if (lines_filled > 0) {
                    // window.dispatcher.dispatch_aj-create a new current_line
                    // dispatch-update its ship pt and qty
                    w.warn('insert line from ship_pt_dialog not implemented');
                }
                ship_from_id = w.query_result[result_idx]['id'];
                ship_from_label = w.query_result[result_idx]['title'];

                ship_pt_field = getElementMatchingName(current_line, 'req_lineship_pt');
                qty_ordered_field = getElementMatchingName(current_line, 'req_lineqty_ordered');
                qty_shipped_field = getElementMatchingName(current_line, 'req_linequantity');
                if (ship_pt_field) {
                    ship_pt_field.setValue(ship_from_id);
                    current_line.widget.queue_update(ship_pt_field, ship_pt_field.widget.data, null);
                    var ship_pt_label = document.getElementById(ship_pt_field.name);
                    if (ship_pt_label) {
                        ship_pt_label.setLabel(ship_from_label);
                    }
                    lines_filled++;
                } else {
                    w.error("Cannot find ship_pt field in current line "+current_line.id);
                }

                // ### quote specific -- on
                if (qty_ordered_field) {
                    qty_ordered_field.focus();
                    qty_ordered_field.setValue(qty_wanted);
                    qty_ordered_field.blur();
                    current_line.widget.queue_update(qty_ordered_field, qty_ordered_field.widget.data, null);
                } else {
                    w.error("Cannot find qty_ordered field in current line "+current_line.id);
                }
                if (qty_shipped_field) {
                    qty_shipped_field.focus();
                    qty_shipped_field.setValue(qty_wanted);
                    qty_shipped_field.blur();
                    current_line.widget.queue_update(qty_shipped_field, qty_shipped_field.widget.data, null);
                } else {
                    w.error("Cannot find qty_shipped field in current line "+current_line.id);
                }

                // update datagrid totals
                window.dispatcher.dispatch('change', qty_ordered_field, update_totals, qty_ordered_field.widget.data, 'Selected Ship Point: Update Totals');
            }
            result_idx++;
        } // for(i = 0)
        // w.debug(inDlgElement.innerHTML);

        w.hide();
        w.erase();
        w.restore_focus.focus(); // should be in w.hide()
        w.restore_focus = null;

    } // save_close

}  // bbship_pt_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) {
        debug('alias:'+alias + ' table:'+table);
        return alias.substring(table.length);
    } // bbfield.field_name_for_alias_name

    // Private
    // lookup our record's data in window.recordmap
    this._get_recordmap = function() {
        var d = null;
        if (this.prefix && (this.prefix != '')) {
            d = window.recordmap[this.prefix];
        } else {
            // top level record
            // Must match view/template_fns.php::print_record_model() usage
            d = window.recordmap['_HEAD_'];
        }
        return d;
    } // bbfield._get_recordmap

    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;
    }

    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);
        }
    } 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');

} // bbfield_req_line

// 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);
        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 row_widget = inElement.widget.record;
        if (!row_widget.queue) {
            row_widget.queue = row_widget.create_queue(inElement.widget.data);
        }
        row_widget.debug('queueing up '+inElement.name+' with value '+inElement.getValue());
        row_widget.queue[inElement.name] = inElement.getValue();

        var row = row_widget.elem;

        // HACK: always queue the extended prices.
        // compensate for onChange not firing when we recalc prices
        /*
        if (inElement.name.indexOf('qty') > -1) {
            var extprice = getElementMatchingName(row, 'extended_price]');
            if (extprice) {
                // an update_total or equivilent should already be dispatched
                window.dispatcher.dispatch('change', extprice, row_widget.queue_update, {}, 'Expect Ext Price Change');
            }
        }
         */

        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);
        }
        return false;

    } // queue_update

    // Event
    // destroy this record
    // trigger immediate send to server
    // ### unclear that json_receive supports _aj_delete response
    this.queue_delete = function(inElement, inArgList, inEvent) {
        var row_widget = inElement.widget.record;
        if (!row_widget.queue) {
            row_widget.queue = row_widget.create_queue(inElement.widget.data);
        }

        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);

        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
            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.debug('old_row.queue: '+to_string(old_row.widget.queue));
                    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');
    } // 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) {
        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?
            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) {
        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) {
            // ### would like bundle support so server
            // can issue multiple record changes
            w._dispatch(otxt, inArgs);
        }
    } // json_dispatch

    // support json_dispatch().  xfer 'this' to our context
    this._dispatch = function (otxt, inArgs) {
        window.debug("bbrecord._dispatch otxt.receive: '"+otxt.receive+"'");
        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
            // 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']);
        }

    } //_dispatch

    // add new record to display
    // ### not for req_line
    this.receive_insert = function(inResponse, inArgs) {
        this.error('bbrecord.receive_insert not implemented');
        var evt = this.dispatcher.apply_feature(this.part_prefix, 'pre_receive_insert', this);
        if (evt == this.dispatcher.event_error) {
            return false;
        } else if (evt == this.dispatcher.event_handled) {
            return true;
        }
        this.receive_update(inResponse, inArgs);

        this.dispatcher.apply_feature(this.part_prefix, 'post_receive_insert', this);
    } // receive_insert

    // store changes to this record
    this.receive_update = function(inResponse, inArgs) {
        this.debug('bbrecord.receive_update');
        var row = this.elem;
        if (row) {
            var evt = this.dispatcher.apply_feature(this.part_prefix, 'pre_receive_update', this);
            if (evt == this.dispatcher.event_error) {
                return false;
            } else if (evt == this.dispatcher.event_handled) {
                return true;
            }
            this.fill_row(inArgs, inResponse, false);
            // ### HACK workaround external event handler dissappearing
            if (inResponse.table == 'address') {
                this.debug('calling lookup_tax_rate()');
                setTimeout(lookup_tax_rate, 100);
            }
            this.dispatcher.apply_feature(this.part_prefix, 'post_receive_update', this);
        } else {
            this.error("inElement is not in a bbrecord ("+inElement.tagName+") " + JSON.stringify(otxt));
        }
    } // receive_update

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

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

    // copy from inResponse to our bbfields
    // part of receive_update
    this.fill_row = function(inArgs, inResponse, inInsert) {
        inRow = this.elem;
        var nl_inputs = inRow.getElementsByTagName('input');
        var nl_selects = inRow.getElementsByTagName('select');
        var nl_textareas = inRow.getElementsByTagName('textarea');
        var inputs = new Array();
        append_nodelist(inputs, nl_inputs, true);
        append_nodelist(inputs, nl_selects);
        append_nodelist(inputs, nl_textareas);
        debug_msg('nl_inputs.length: '+nl_inputs.length + ' inputs.length: '+inputs.length);
        // ### select textarea
        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);

                    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);
                    }
                    if (inInsert) {
                        this.error('insert not implemented in bbrecord');
                    }
                } // 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;
            var pfx = this.elem.field_list[0].prefix;
            if (pfx && (pfx != ''))  {
            } else {
                pfx = '_HEAD_';
            }
            this.dispatcher.debug('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.dispatcher.register('change', this.elem.field_list[i].elem, this.queue_update, d, 'change_bbfield_queue_update');
        }


    } // post_widgetize

    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 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.className;
    }
    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');

    // wait for any AJ operations to complete
    // before submitting this.form
    // ### queue the submit
    this.wait_submit = function() {
        if ((window.dispatcher.commit > 0) && (window.request_queue.length > 0)) {
            info("commit queue size: " + window.request_queue.length);
            var el = this;
            setTimeout(function() { el.wait_submit() }, 300);
        } else {
            // reload
            window.dispatcher.commit = 2;
            error('commit submit disabled for debugging');
            this.form.submit();
        }
    } // wait_submit

    this.flush = function() {
        // close all dialogs
        // ### need a convenient way to identify all dialogs in a page.
        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(); }

        // 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++) {
            if (records[r].widget.queue) {
                info('commit flushing '+to_string(records[r].widget.queue));
                var fev = {type: 'blur'} ;
                records[r].widget.queue_send(records[r], records[r].widget.json_dispatch, fev);
            } else {
                info('commit skipping '+records[r].tagName);
            }

        }
        this.wait_submit();
    } // flush

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

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

// 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();
    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();
    }

}

// onload handler
function bbinit() {
    window.dispatcher = new dispatcher();
    if (window.bbkeymap_init) {
        bbkeymap_init(window);
    } else {
        debug_msg(window.name + ' is missing');
        window.error('cannot find bbkeymap_init');
    }
    rationalize_form_elements();
    window.widgetize(window.document);
}

// 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();
        return false;
    } else {
        window.debug("cannot find window '"+inWinID+"'");
    }
}

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


// Assign consistent accessors to form element prototypes.
// Minimize JS side interface differences among form elements.
function rationalize_form_elements(inWidgetElem) {
    var inputs = document.getElementsByTagName('input');
    var selects = document.getElementsByTagName('select');
    var textareas = document.getElementsByTagName('textarea');

    if (inputs && inputs.length) {
        inputs[0].constructor.prototype.setValue = _input_setValue;
        inputs[0].constructor.prototype.getValue = _input_getValue;
    }
    if (selects && selects.length) {
        selects[0].constructor.prototype.setValue = _select_setValue;
        selects[0].constructor.prototype.getValue = _select_getValue;
    }
    if (textareas && textareas.length) {
        textareas[0].constructor.prototype.setValue = _textarea_setValue;
        textareas[0].constructor.prototype.getValue = _textarea_getValue;
    }
}

// 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 _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;
}

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'; // vs field_name or prefix_name

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

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

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

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

    // constructor
    var num_results = inResultSet.length;
    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 = '';
        for(var j = 0; j < num_labels; j++) {
            ctxt = ctxt + '<td id="'+ this.name + i +'_'+ j + '">';
            if (inLabel[j]['format']) {
                switch(inLabel[j]['format']) {
                case 'input_text':
                    ctxt = ctxt + this.format_input_text(inResultSet[i][ inLabel[j][this.field_column] ], inLabel[j]);
                    break;
                case 'input_hidden':
                    ctxt = ctxt + this.format_input_hidden(inResultSet[i][ inLabel[j][this.field_column] ], inLabel[j]);
                    break;
                case 'money':
                    ctxt = ctxt + this.format_display_only(inResultSet[i][ inLabel[j][this.field_column] ], inLabel[j]);
                    break;
                default:
                    ctxt = ctxt + this.format_display_only(inResultSet[i][ inLabel[j][this.field_column] ], inLabel[j]);
                    break;
                }
            } else {
                ctxt = ctxt +  this.format_display_only(inResultSet[i][ inLabel[j][this.field_column] ], inLabel[j]);
            }
            ctxt = ctxt + '</td>';
        }
        txt = txt + ctxt + '</tr>';

    }
    this.html = this.html + txt + '</table>';

} // 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().
        window.dispatcher.debug('_keystroke here');
        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, 'normal', 'hilight');
    }
    this.unhighlight_row = function(inElem) {
        replace_class(inElem, 'hilight', 'normal');
    }

    // Event
    // handle up arrow
    this.key_up = function (inTopElem, inArgs, inEventObj) {
        inArgs._key_up(inTopElem, inEventObj); // this
    }
    // 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;
        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++) {
                window.dispatcher.debug('apply_feature: 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.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.observer

// link up the fvlogger into current class
// usage: bbtool.debug_mixin.apply(this, []);
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.
// 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.bbclassname + "\");'>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'>";
        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='toolbar'>";
        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('class',  (inParam.bbclassname + " " + inParam.cssclassname + " 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];
            }
        }
        window.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
    this.destroy_dialog = function(inDialogElem) {
        this.dialog_root.removeChild(inDialogElem);
    }

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