Source: js/widget.js

/** @namespace Widget
    @desc      Handles loading and management of Widget-specific content, ensuring all necessary information is present.
               Detect dynamically added widgets with Widget.detect(), the rest is automatic.
               Definitions, flags, and instances stored by type in Widget.types.
               Definitions require title, onRun, and onReconnect.
*/
var Widget = Widget ||
{
    /* properties - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
    
    /** @member {String} Widget.root
        @readonly
        @desc   Path to widgets directory.
    */
    'root'    : PATH.substring(0, PATH.lastIndexOf('/')) + '/widgets',
    
    /** @member  {Boolean} Widget.detected
        @private
        @desc    Whether the detect method has been called at least once.
    */
    'detected': false,
    
    /** @member   {Object} Widget.types
        @readonly
        @desc     Index of all Widget types that have been detected.
    */
    'types'   : {},
    
    /* classes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    
    /**
        @class Widget.ControllerClass
        @desc  Instantiated as a controller object for each Widget instance.
               Contains methods for acquiring data, selecting elements within Widget, and relevant properties.
    */
    'ControllerClass': function ($element)
    {
        /* properties --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  */
        
        //  private      public            value
        
        var instance                     = this;
        
        /** @member   {jQuery} Widget.ControllerClass#$element
            @readonly
            @desc     Reference to this Widget's containing element.
        */
        var $element   = this.$element   = jWrap($element);
        
        /** @member   {Object} Widget.ControllerClass#attributes
            @readonly
            @desc     Index of attributes provided via Widget tag or {@link Widget.create} parameters.
            @todo     Make these dynamic as attributes are modified.
        */
        var attributes = this.attributes = getAttributes($element);
        
        /** @member   {String} Widget.ControllerClass#type
            @readonly
            @desc     Type of this Widget.
        */
        var type       = this.type       = attributes.type;
        
        /** @member   {String} Widget.ControllerClass#prefix
            @readonly
            @desc     {@link Widget.ControllerClass#type} + '_'.
        */
        var prefix     = this.prefix     = type + '_';
        
        /** @member   {String} Widget.ControllerClass#layout
            @readonly
            @desc     Name of main layout template for this Widget, initially {@link Widget.ControllerClass#prefix} + '_'.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        var layout     = this.layout     = prefix + 'layout'; // override in definition/tag
        
        /** @member   {String} Widget.ControllerClass#root
            @readonly
            @desc     Path to directory of resources for this Widget type.
        */
                         this.root       = Widget.root + '/' + type;
        
        /** @member   {Number} Widget.ControllerClass#uniqueId
            @readonly 
            @desc     A numeric identifier by Widget type.
        */
        var uniqueId   = this.uniqueId   = Widget.types[this.type].instances.length + 1;
        
        /** @member   {String} Widget.ControllerClass#uniqueName
            @readonly
            @desc     {@link Widget.ControllerClass#type} + {@link Widget.ControllerClass#uniqueId}
        */
        var uniqueName = this.uniqueName = type + ':' + uniqueId;
        
        /** @member   {String} Widget.ControllerClass#eventScope
            @readonly
            @desc     '.widget_' + {@link Widget.ControllerClass#prefix} + {@link Widget.ControllerClass#uniqueId}
        */
        var eventScope = this.eventScope = '.widget_' + prefix + uniqueId;
        
        /* developer callbacks --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  -- */
        
        /** @member   {callbackGeneric} Widget.ControllerClass#onRun
            @abstract
            @desc     Developer callback.
                      Behavioral starting point, fired on successful initialization of Widget controller.
                      This includes complete loading of templates and scripts, but not multimedia or styles.
                      Will not be retriggered by network reconnection, see {@link Widget.ControllerClass#onReconnect} for this functionality.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
            @see      {@link Widget.onReconnect}
        */
        this.onRun = function () {};
        
        /** @member   {callbackGeneric} Widget.ControllerClass#onReconnect
            @abstract
            @desc     Developer callback.
                      Fired on reestablishment of User Server XMPP connection.
                      Use this event to refresh lists of data, and to reenable user interfaces that were disabled while offline.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
            @see      {@link Widget.onRun}
        */
        this.onReconnect = function () {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletVisible
            @abstract
            @desc     Developer callback. Fired on change of visibility of Starlet to the user.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletVisible = function (state) {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletActive
            @abstract
            @desc     Developer callback. Fired on change of whether Starlet has primary focus.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletActive = function (state) {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletFocused
            @abstract
            @desc     Developer callback. Fired on change of whether Starlet's application window has focus.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletFocused = function (state) {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletMinimized
            @abstract
            @desc     Developer callback. Fired on change of whether Starlet's application window is minimized.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletMinimized = function (state) {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletHidden
            @abstract
            @desc     Developer callback. Fired on change of whether Starlet's application window is hidden.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletHidden = function (state) {};
        
        /** @member   {callbackFrameworkEvent} Widget.ControllerClass#onStarletMouse
            @abstract
            @desc     Developer callback. Fired on change of whether Starlet's WebView contains the mouse pointer.
                      Override in custom Widget definition.
            @see      {@link Widget.define}
        */
        this.onStarletMouse = function (state) {};
        
        /* wrapper methods --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  -- */
        
        /** @method    Widget.ControllerClass#find
            @readonly
            @desc      Wrapper method for jQuery's find, with context forced to Widget element.
            
            @arg       {...Mixed} arguments - Selectors to pass to jQuery's find method.
            @returns   {jQuery}   element(s)
            
            @see       {@link http://api.jquery.com/find/}
        */
        this.find     = function () { return $element.find.apply($element, arguments); };
        
        /** @method    Widget.ControllerClass#lightbox
            @readonly
            @chainable
            @desc      Opens a lightbox overlay with specific location and dimensions.
            
            @arg       {String} name    - Framework name of new WebView.
            @arg       {String} url     - Location to open in lightbox.
            @arg       {Number} width   - Width at which to open lightbox.
            @arg       {Number} height  - Height at which to open lightbox.
            @returns   {Object} {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#lightbox(2)}
            @see       {@link OrionLightbox.open}
        */
        /** @method    Widget.ControllerClass#lightbox(2)
            @readonly
            @chainable
            @desc      Closes a lightbox overlay by name.
            
            @arg       {String} name    - Framework name of WebView to close.
            @returns   {Object} {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#lightbox}
            @see       {@link OrionLightbox.close}
            @see       {@link OrionLightbox.setClosedCallback}
        */
        this.lightbox = function () { Widget.lightbox.apply(Widget  , arguments); return instance; };
        
        /** @method    Widget.ControllerClass#modal
            @readonly
            @chainable
            @desc      Creates a Bootstrap modal dialog in a manner that forces a consistent z-index.
            
            @arg       {String}         html                        - Raw html to pass through to Bootstrap modal method.
            @arg       {Object}         [options={}]                - Additional options.
            @arg       {String}         [options.className]         - Custom CSS class name to add to modal element.
            @arg       {Boolean|String} [options.backdrop='static'] - Whether to include backdrop,
                                                                      and to allow click to dismiss.
            @arg       {Boolean}        [options.keyboard=true]     - Whether to allow esc to dismiss.
            @arg       {Boolean}        [options.show=true]         - Whether to show immediately.
            @arg       {Boolean|String} [options.remote=false]      - Remote content URL.
            @returns   {Object}         {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#modal(2)}
            @see       {@link http://getbootstrap.com/2.3.2/javascript.html#modals}
        */
        /** @method    Widget.ControllerClass#modal(2)
            @readonly
            @chainable
            @desc      Controls Bootstrap modal dialog.
            
            @arg       {String} action - Action (show|hide|toggle) to pass through to Bootstrap modal method.
            @returns   {Object} {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#modal}
            @see       {@link http://getbootstrap.com/2.3.2/javascript.html#modals}
        */
        this.modal    = function () { Widget.modal   .apply(Widget  , arguments); return instance; };
        
        /** @method    Widget.ControllerClass#on
            @readonly
            @chainable
            @desc      Wrapper method for jQuery's on, with context forced to Widget element.
            
            @arg       {...Mixed} arguments - Selectors, event names, or event handlers to pass to jQuery's on method.
            @returns   {Object} {@link Widget.ControllerClass}
            
            @see       {@link http://api.jquery.com/on/}
        */
        this.on       = function () { $element.on    .apply($element, arguments); return instance; };
        
        /* methods --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  -- */
        
        /** @method    Widget.ControllerClass#speak
            @readonly
            @chainable
            @desc      Emit data under a specific packet name to any Widget listening for it and this Widget type.
            
            @arg       {String}       packetName - Topic to target specific listeners.
            @arg       {Object|Array} data       - The item to broadcast.
            @returns   {Object}       {@link Widget.CopntrollerClass}
            
            @see       {@link Widget.ControllerClass#broadcast}
            @see       {@link Widget.ControllerClass#listen}
            @see       {@link Widget.ControllerClass#ignore}
        */
        this.speak = function (packetName, data)
        {
            log(L_INFO, 'Widget', uniqueName, 'speak', arguments);
            
            var widgets = Widget.instances(),
                type    = instance.type,
                packet  = {'widget': instance.type, 'name': packetName, 'data': typeof data !== 'undefined' ? data : {}};
            
            $.each(widgets, function ()
            {
                this.$element.triggerHandler(instance.type + '_' + packetName, [packet]);
            });
            
            Starlet.$content.triggerHandler(instance.type + '_' + packetName, [packet]);
            
            return instance;
        };
		
        /** @method    Widget.ControllerClass#broadcast
            @readonly
            @chainable
            @desc      Emit data under a specific packet name to any Widget listening for it,
                       regardless of this Widget type.
            
            @arg       {String}       packetName - Topic to target specific listeners.
            @arg       {Object|Array} data       - The item to broadcast.
            @returns   {Object}       {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#speak}
            @see       {@link Widget.ControllerClass#listen}
            @see       {@link Widget.ControllerClass#ignore}
        */
		this.broadcast = function (packetName, data)
		{
			log(L_DEBUG, 'Widget', uniqueName, 'broadcast', arguments);
			
			var widgets			= Widget.instances(),
                type			= instance.type,
                packet			= {'widget': instance.type, 'name': packetName, 'data': typeof data !== 'undefined' ? data : {}},
				handlerName 	= '_{0}'.format(packetName);
			
            $.each(widgets, function () { this.$element.triggerHandler(handlerName, [packet]); });
            
            Starlet.$content.triggerHandler(handlerName, [packet]);
            
            return instance;
		};
        
        /** @method    Widget.ControllerClass#listen
            @readonly
            @chainable
            @desc      Listen for data from a specific Widget type and of a specific packet name.
            
            @arg       {String}   widgetType - Type of Widget to listen for data from.
            @arg       {String}   packetName - Topic to listen for.
            @arg       {Function} callback   - Function to respond to incoming data.
            @returns   {Object}   {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#speak}
            @see       {@link Widget.ControllerClass#broadcast}
            @see       {@link Widget.ControllerClass#ignore}
        */
        this.listen = function (widgetType, packetName, callback)
        {
            log(L_DEBUG, 'Widget', uniqueName, 'listen', arguments);
            
			!widgetType && (widgetType = '');
			
            instance.$element.on(widgetType + '_' + packetName, function (e, packet) { callback(packet); });
            
            return instance;
        };
        
        /** @method    Widget.ControllerClass#ignore
            @readonly
            @chainable
            @desc      Stop listening for data from a specific Widget type and of a specific packet name.
            
            @arg       {String}   widgetType - Type of Widget to stop listening for data from.
            @arg       {String}   packetName - Topic to stop listening for.
            @returns   {Object}   {@link Widget.ControllerClass}
            
            @see       {@link Widget.ControllerClass#speak}
            @see       {@link Widget.ControllerClass#broadcast}
            @see       {@link Widget.ControllerClass#listen}
        */
        this.ignore = function (widgetType, packetName)
        {
            log(L_DEBUG, 'Widget', uniqueName, 'ignore', arguments);
            
            instance.$element.off(widgetType + '_' + packetName);
            
            return instance;
        };
        
        /** @method   Widget.ControllerClass#ask
            @readonly
            @desc     Request data with criteria, or send a data-related instruction expecting a result. Data on demand
                      (action), future updates (subscribe), or both can be acquired via this single method.
            
            @arg      {String}   packet           - Name of {@link OrionObjects.DataPacket} being requested.
            @arg      {Object}   meta             - Criteria and information about the request. Besides the properties
                                                    defined below, each packet type expects varying additional criteria.
            @arg      {String}   [meta.action]    - Action to be performed immediately (e.g. 'list', 'get', etc.).
            @arg      {Boolean}  [meta.subscribe] - Whether to receive future updates.
            @arg      {Function} [meta.callback]  - Function to respond to incoming data. May also be associated via the
                                                    promise's then method, though it will only resolve once. Remember to
                                                    specify the context of the callback or include a closure so it can
                                                    access its controller.
            @returns  {Object}   promise
            
            @see      {@link Widget.ControllerClass#tell}
            @see      {@link Bus.ask}
            @see      {@link Bus.tell}
            @see      {@link http://api.jquery.com/category/deferred-object/}
            @example  Bus.ask('users',
{
    'action'   : 'list',
    'subscribe': true,
    'callback' : controller.onUserData.bind(controller)
});
            @example  controller.ask('users', {'action': 'list'}).then(controller.onUserData.bind(controller));
        */
        this.ask = function (packet, meta)
        {
            log(L_DEBUG, 'Widget', uniqueName, 'ask', arguments);
            
            var controller = this;
            
            meta                  = meta || {};
            meta.widgetType       = controller.type;
            meta.widgetUniqueId   = controller.uniqueId;
            meta.widgetUniqueName = controller.uniqueName;
            
            return Bus.ask(packet, meta);
        };
        
        /** @method   Widget.ControllerClass#tell
            @readonly
            @desc     Send a data-related instruction, expecting no result.
            
            @arg      {String}   packet           - Name of {@link OrionObjects.DataPacket} being requested.
            @arg      {Object}   meta             - Criteria and information about the request. Besides action,
                                                    each packet type expects varying additional criteria.
            @arg      {String}   [meta.action]    - Action to be performed (e.g. 'list', 'get', etc.).
            @returns  {Boolean} true
            
            @see      {@link Widget.ControllerClass#ask}
            @see      {@link Bus.ask}
            @see      {@link Bus.tell}
            @example  controller.tell('usergroups',
{
    'action': 'create',
    'name'  : 'Foo',
    'users' : [1, 2, 3]
});
        */
        this.tell = function (packet, meta)
        {
            log(L_DEBUG, 'Widget', uniqueName, 'tell', arguments);
            
            var controller = this;
            
            meta                  = meta || {};
            meta.widgetType       = controller.type;
            meta.widgetUniqueId   = controller.uniqueId;
            meta.widgetUniqueName = controller.uniqueName;
            
            return Bus.tell(packet, meta);
        };
        
//         this.listen = function (packet, callbackResponder)
//         {
//             log(L_DEBUG, 'Widget', uniqueName, 'listen', arguments);
//             
//             console.log(arguments);
//             
//             instance.$element.on(packet, function (e, data, callbackAsker, deferred)
//             {
//                 log(L_DEBUG, 'Widget', uniqueName, 'listen lambda', arguments);
//                 
//                 var data     = data || {},
//                     response = callbackResponder.apply(instance, data);
//                 
//                 callbackAsker && callbackAsker(response);
//                 deferred      && deferred.resolve(response);
//             });
//             
//             return instance;
//         };
//         
//         this.ignore = function (packet)
//         {
//             log(L_DEBUG, 'Widget', uniqueName, 'ignore', arguments);
//             
//             instance.$element.off(packet);
//             
//             return instance;
//         };
        
//         this.options = function ()
//         {
//             log(L_DEBUG, 'Widget', uniqueName, 'options', arguments);
//             
//             var self = arguments.callee,
//                 args = Array.prototype.slice.call(arguments);
//             
//             self.controller = instance;
//             args[0]         = 'widget.' + this.type + '.' + args[0];
//             
//             return Bus.options.apply(Bus, args);
//         };
        
        /* construct --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  -*/
        
        $element.data('controller', instance); // make accessible via dom
        
        Widget.types[type].synchronizer.done(function ()
        {
            $.extend(true, instance, Widget.types[type].definition, attributes); // deep extend
            
            layout in templates
                && $element.append(templates[layout](instance));
            
            instance
                .listen('Starlet', 'starletVisible'  , function (packet) { instance.onStarletVisible   && instance.onStarletVisible   (packet.data); })
                .listen('Starlet', 'starletActive'   , function (packet) { instance.onStarletActive    && instance.onStarletActive   (packet.data); })
                .listen('Starlet', 'starletFocused'  , function (packet) { instance.onStarletFocused   && instance.onStarletFocused  (packet.data); })
                .listen('Starlet', 'starletMinimized', function (packet) { instance.onStarletMinimized && instance.onStarletMinimized(packet.data); })
                .listen('Starlet', 'starletHidden'   , function (packet) { instance.onStarletHidden    && instance.onStarletHidden   (packet.data); })
                .listen('Starlet', 'starletMouse'    , function (packet) { instance.onStarletMouse     && instance.onStarletMouse    (packet.data); });
            
            log(L_MAIN, 'Widget', uniqueName, 'onRun')
                && instance.onRun();
        });
        
        log(L_INFO, 'Widget.ControllerClass', 'new instance', type + ':' + uniqueId);
    },
    
    /* callbacks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    
    /** @member  {callbackGeneric} Widget~onDisconnect
        @private
        @desc    Fired on termination of User Server XMPP connection.
                 Facilitates event cascade to all Widgets.
        @see     {@link Widget.ControllerClass#onDisconnect}
        @see     {@link Starlet.onDisconnect}
        @see     {@link Widget~onReconnect}
        @see     {@link Widget~onUnload}
        @see     {@link Widget~onExit}
    */
    'onDisconnect': function ()
    {
        log(L_MAIN, 'Widget.onDisconnect');
        
        var args = Array.prototype.slice.call(arguments);
        
        this.instances().map(function (instance, index)
        {
            log(L_FUNC, 'Widget.ControllerClass', instance.type, 'onDisconnect', args)
                && instance.onDisconnect
                && instance.onDisconnect.apply(instance, args);
        });
    },
    
    /** @member  {callbackGeneric} Widget~onReconnect
        @private
        @desc    Fired on reestablishment of User Server XMPP connection.
                 Facilitates event cascade to all Widgets.
        @see     {@link Widget.ControllerClass#onReconnect}
        @see     {@link Starlet.onReconnect}
        @see     {@link Widget~onDisconnect}
        @see     {@link Widget~onUnload}
        @see     {@link Widget~onExit}
    */
    'onReconnect': function ()
    {
        log(L_MAIN, 'Widget.onReconnect');
        
        var args = Array.prototype.slice.call(arguments);
        
        this.instances().map(function (instance, index)
        {
            log(L_FUNC, 'Widget.ControllerClass', instance.type, 'onReconnect', args)
                && instance.onReconnect
                && instance.onReconnect.apply(instance, args);
        });
    },
    
    /** @member  {callbackGeneric} Widget~onUnload
        @private
        @desc    Fired on reestablishment of User Server XMPP connection.
                 Facilitates event cascade to all Widgets.
        @see     {@link Widget.ControllerClass#onRemove}
        @see     {@link Starlet.onUnload}
        @see     {@link Widget~onDisconnect}
        @see     {@link Widget~onReconnect}
        @see     {@link Widget~onExit}
    */
    'onUnload': function ()
    {
        log(L_MAIN, 'Widget.onUnload');
        
        $.each(this.instances(), function (index, controller)
        {
            Widget.remove(controller);
        });
    },
    
    /** @member  {callbackGeneric} Widget~onExit
        @private
        @desc    Fired before unloading Starlet, but only when Framework is about to exit.
                 Expect no more than ~200ms before WebView stops.
                 Facilitates event cascade to all Widgets.
        @see     {@link Widget.ControllerClass#onExit}
        @see     {@link Starlet.onExit}
        @see     {@link Widget~onDisconnect}
        @see     {@link Widget~onReconnect}
        @see     {@link Widget~onUnload}
    */
    'onExit': function ()
    {
        log(L_MAIN, 'Widget.onExit');
        
        var args = Array.prototype.slice.call(arguments);
        
        this.instances().map(function (instance, index)
        {
            log(L_FUNC, 'Widget.ControllerClass', instance.type, 'onExit', args)
                && instance.onExit
                && instance.onExit.apply(instance, args);
        });
    },
    
    // 'onFrameworkFocus': function ()
    // {
    //     log(L_MAIN, 'Widget.onFrameworkFocus');
        
    //     var args = Array.prototype.slice.call(arguments);
        
    //     this.instances().map(function (instance, index)
    //     {
    //         log(L_FUNC, 'Widget.ControllerClass', instance.type, 'onFrameworkFocus', args)
    //             && instance.onFrameworkFocus
    //             && instance.onFrameworkFocus.apply(instance, args);
    //     });
    // },
    
    // 'onFrameworkMinimize': function ()
    // {
    //     log(L_MAIN, 'Widget.onFrameworkMinimize');
        
    //     var args = Array.prototype.slice.call(arguments);
        
    //     this.instances().map(function (instance, index)
    //     {
    //         log(L_FUNC, 'Widget.ControllerClass', instance.type, 'onFrameworkMinimize', args)
    //             && instance.onFrameworkMinimize
    //             && instance.onFrameworkMinimize.apply(instance, args);
    //     });
    // },
    
    /* wrapper methods - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    
    /** @method    Widget.lightbox
        @readonly
        @chainable
        @desc      Opens a lightbox overlay with specific location and dimensions.
        
        @arg       {String} name   - Framework name of new WebView.
        @arg       {String} url    - Location to open in lightbox.
        @arg       {Number} width  - Width at which to open lightbox.
        @arg       {Number} height - Height at which to open lightbox.
        @returns   {Object} {@link Starlet}
        
        @see       {@link Widget.lightbox(2)}
        @see       {@link Widget.ControllerClass#lightbox}
        @see       {@link Widget.ControllerClass#lightbox(2)}
        @see       {@link Starlet.lightbox}
        @see       {@link Starlet.lightbox(2)}
        @see       {@link OrionLightbox.open}
    */
    /** @method    Widget.lightbox(2)
        @readonly
        @chainable
        @desc      Closes a lightbox overlay by name.
        
        @arg       {String} name - Framework name of WebView to close.
        @returns   {Object} {@link Starlet}
        
        @see       {@link Widget.lightbox}
        @see       {@link Widget.ControllerClass#lightbox}
        @see       {@link Widget.ControllerClass#lightbox(2)}
        @see       {@link Starlet.lightbox}
        @see       {@link Starlet.lightbox(2)}
        @see       {@link OrionLightbox.close}
        @see       {@link OrionLightbox.setClosedCallback}
    */
    'lightbox': function () { log(L_DEBUG, 'Widget.lightbox', arguments); return Starlet.lightbox.apply(Starlet, arguments); },
    
    /** @method    Widget.modal
        @readonly
        @chainable
        @desc      Creates a Bootstrap modal dialog in a manner that forces a consistent z-index.
        
        @arg       {String}         html                        - Raw html to pass through to Bootstrap modal method.
        @arg       {Object}         [options={}]                - Additional options.
        @arg       {String}         [options.className]         - Custom CSS class name to add to modal element.
        @arg       {Boolean|String} [options.backdrop='static'] - Whether to include backdrop,
                                                                  and to allow click to dismiss.
        @arg       {Boolean}        [options.keyboard=true]     - Whether to allow esc to dismiss.
        @arg       {Boolean}        [options.show=true]         - Whether to show immediately.
        @arg       {Boolean|String} [options.remote=false]      - Remote content URL.
        @returns   {Object}         {@link Starlet.$modal}
        
        @see       {@link Widget.modal(2)}
        @see       {@link Widget.ControllerClass#modal}
        @see       {@link Widget.ControllerClass#modal(2)}
        @see       {@link Starlet.modal}
        @see       {@link Starlet.modal(2)}
        @see       {@link http://getbootstrap.com/2.3.2/javascript.html#modals}
    */
    /** @method    Widget.modal(2)
        @readonly
        @chainable
        @desc      Controls Bootstrap modal dialog.
        
        @arg       {String} action - Action (show|hide|toggle) to pass through to Bootstrap modal method.
        @returns   {Object} {@link Starlet.$modal}
        
        @see       {@link Widget.modal}
        @see       {@link Widget.ControllerClass#modal}
        @see       {@link Widget.ControllerClass#modal(2)}
        @see       {@link Starlet.modal}
        @see       {@link Starlet.modal(2)}
        @see       {@link http://getbootstrap.com/2.3.2/javascript.html#modals}
    */
    'modal'   : function () { log(L_DEBUG, 'Widget.modal'   , arguments); return Starlet.modal.apply(Starlet, arguments); },
    
    /* methods - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    
    /** @method    Widget.add
        @readonly
        @desc      Creates a Widget of specified type within the Starlet.
        @todo       Make {@link Widget} a constructor to replace {@link Widget.add}/{@link Widget.create},
                    make {@link Widget.remove} a method of instantiated Widget controllers.
        @deprecated since version 2.1
        
        @arg       {String}         type                            - Type of widget to load resources for,
                                                                      must exist as a subdirectory in 'widgets'.
        @arg       {jQuery|Element} [$context=$('section#content')] - Element to act as root.
        
        @see       {@link Widget.create}
        @see       {@link Widget.remove}
    */
    'add': function (type, $context)
    {
        log(L_FUNC, 'Widget.add', arguments);
        
        $context = $context ? jWrap($context) : $('section#content');
        $context.append('<widget type="' + type + '"></widget>');
        this.detect();
    },
    
    /** @method     Widget.define
        @readonly
        @desc       Extends Widget with custom properties, methods, and callbacks.
                    Call this in your Widget directory's index.js, it signifies to the synchronizer that fundamental
                    custom Widget resources have been loaded and parsed.
        @todo       Document definition,
                    Make {@link Widget} a constructor to replace {@link Widget.add}/{@link Widget.create},
                    make {@link Widget.remove} a method of instantiated Widget controllers.
        @deprecated since version 2.1
        
        @arg        {Object} definition - Structure with which to extend Widget,
                                          must include override for onRun to initiate any behavior.
        @returns    {Object} Widget definition reference.
        
        @see        {@link Widget.ControllerClass#attributes}
        @see        {@link Widget.ControllerClass#onRun}
        @see        {@link Widget.ControllerClass#onDisconnect}
        @see        {@link Widget.ControllerClass#onReconnect}
        @see        {@link Widget.ControllerClass#onUnload}
        @see        {@link Widget.ControllerClass#onExit}
        @see        {@link Widget.ControllerClass#onStarletVisible}
        @see        {@link Widget.ControllerClass#onStarletActive}
        @see        {@link Widget.ControllerClass#onStarletFocused}
        @see        {@link Widget.ControllerClass#onStarletMinimized}
        @see        {@link Widget.ControllerClass#onStarletHidden}
        @see        {@link Widget.ControllerClass#onStarletMouse}
    */
    'define': function (definition)
    {
        log(L_FUNC, 'Widget.define', arguments);
        
        // get type dynamically via src
        
        var $scripts   = $('head script[type="text/javascript"]'),
            $script    = $($scripts[$scripts.length - 1]),
            path       = $script.attr('src').split('/'),
            pathLength = path.length,
            type       = null;
        
        for (var i = 0; i < pathLength; i++)
            if (path[i] === 'widgets' || path[i] === Starlet.identifier) { type = path[i + 1]; break; }
        
		// see if this is a private widget from another starlet
		!type && this.types[path[path.length - 2]] && (type = path[path.length - 2]);
		
        if (!type) { log(L_ERROR, 'Widget.define', "can't determine type"); return false; }
        
        log(L_FUNC, 'Widget.define', type);
        
        // save definition, signal
        
        !this.types[type].definition               &&
        (this.types[type].definition = definition) &&
         this.types[type].synchronizer.notify(type, 'defined');
        
        return this.types[type];
    },
	
    /** @method   Widget.detect
        @readonly
        @desc     Queries DOM for any uninitialized widgets, initializes new Widget controllers.
        
        @arg      {jQuery|Element} [scope=undefined] - Root element to query within.
        @returns  {Number}         Count of new Widget types detected.
        
        @see      {@link Widget.define}
        @see      {@link Widget.load}
    */
    'detect': function (scope)
    {
        log(L_FUNC, 'Widget.detect', arguments);
        
        this.detected = true;
        
        var $widgets      = scope ? $(scope).find('widget') : $('section#content widget'),
            widgetsLength = $widgets.length,
            typesUnique   = [],
            typesNew      = [];
        
        for (var w = 0; w < widgetsLength; w++)
        {
            var $widgetTag = $($widgets[w]),
                attributes = getAttributes($widgetTag),
                type       = attributes.type,
				baseUrl    = attributes.baseurl;
			
			typesUnique.indexOf(type) === -1 && typesUnique.push(type);
			this.types[type] && typesNew.push(type);
			
			var $widgetBox = this.create(type, attributes, baseUrl);
			
            // replace
            
            $widgetTag.replaceWith($widgetBox);
        }
        
        log(L_INFO, 'Widget.detect', '{0} found, {1} unique, {2} new'.format(widgetsLength, typesUnique.length, typesNew.length));
        
        return widgetsLength;
    },
	
    /** @method     Widget.create
        @readonly
        @desc       Creates a Widget of specified type within the Starlet,
                    but in a way that avoids {@link Widget.detect}.
        @todo       Make {@link Widget} a constructor to replace {@link Widget.add}/{@link Widget.create},
                    make {@link Widget.remove} a method of instantiated Widget controllers.
        @deprecated since version 2.1
        
        @arg        {String}         type       - Type of widget to load resources for,
                                                  must exist as a subdirectory in 'widgets'.
        @arg        {Object}         parameters - Attributes that would normally be detected on Widget tag.
        @arg        {String}         [baseUrl]  - Location of widgets directory, if nonstandard.
        @returns    {jQuery|Element} Widget's containing element.
        
        @see        {@link Widget.add}
        @see        {@link Widget.remove}
    */
	'create': function (type, parameters, baseUrl)
	{
        log(L_FUNC, 'Widget.create', arguments);
        
		var $widgetBox = $(templates.widget({})).attr(parameters ? parameters : {}).attr('type', type);
		
		if (!(type in this.types))
		{
			var synchronizer = (new $.Deferred()).progress(function (type, flag)
			{
				log(L_FUNC, 'Widget', type, 'synchronizer', arguments);
				
				var typeStore = Widget.types[type];
				
				switch (flag)
				{
					case 'loaded' : typeStore.flagLoaded  = true; break;
					case 'defined': typeStore.flagDefined = true; break;
					default       :                               break;
				}
				
				typeStore.flagLoaded  &&
				typeStore.flagDefined &&
				typeStore.synchronizer.resolve();
			});
			
			this.types[type] =
			{
				'synchronizer': synchronizer,
				'request'     : null,
				'flagLoaded'  : false,
				'flagDefined' : false,
				'definition'  : null,
				'instances'   : []
			};
			
			this.load(type, baseUrl);
		}
		
		// instantiate
		
		this.types[type].instances.push(new this.ControllerClass($widgetBox));
		
		return $widgetBox;
	},
    
    /** @method   Widget.instances
        @readonly
        @desc     Lists all Widget controllers within Starlet.
        
        @arg      {String}         [typeFilter=''] - Type to list, or undefined for all.
        @returns  {Array.<Object>} Widget controller instances.
        
        @see      {@link Widget.add}
        @see      {@link Widget.create}
        @see      {@link Widget.remove}
    */
    'instances': function (typeFilter)
    {
        log(L_DEBUG, 'Widget.instances', arguments);
        
        var found = [];
        
        for (type in this.types)
        {
            (!typeFilter || type === typeFilter)
                && (found = found.concat(this.types[type].instances));
        }
        
        return found;
    },
    
    /** @method   Widget.load
        @readonly
        @desc     Fetches assets associated with a particular Widget type.
        
        @arg      {String} type       - Type of widget to load resources for,
                                        must exist as a subdirectory in 'widgets'.
        @arg      {String} [baseUrl]  - Location of widgets directory, if nonstandard.
        @returns  {Object} jQuery AJAX request.
        
        @see      {@link Widget.detect}
        @see      {@link Widget.define}
    */
    'load': function (type, baseUrl)
    {
        log(L_FUNC, 'Widget.load', arguments);
        
        // request content
        
        var requestUrl   = [baseUrl ? baseUrl : this.root, type, 'index.html'].join('/'),
            requestTimer = null,
            parseTimer   = null;
        
        log(L_INFO, 'Widget.load', type, 'requesting...', requestUrl)
            && (requestTimer = new TimerClass());
        
        var request = $.ajax(requestUrl, {'type': 'GET'}).success(function (html, status, xhr)
        {
            log(L_INFO, 'Widget.load', type, 'received in ' + requestTimer.elapsed(1), 'parsing...')
                && (parseTimer = new TimerClass());
			
            // insert content
            
            $('head').append($($.trim(html)))
                && log(L_INFO, 'Widget.load', type, 'parsed in ' + parseTimer.elapsed(1));
            
            // process counterparts
            
            detectTemplates();
            detectStyles();
            
            // signal
            
            Widget.types[type].synchronizer.notify(type, 'loaded');
        })
        .error(function (xhr, status, error)
        {
            log(L_ERROR, 'Widget.load', type, status, error, requestUrl);
            
            // scrap instances, stores, elements
            
            if (type in Widget.types)
            {
                log(L_INFO, 'Widget.load', type, 'scrapping')
                    && Widget.types[type].synchronizer.reject();
                
                delete Widget.types[type];
            }
            
            $('div.widget[type="' + type + '"]').remove();
        });
        
        this.types[type].request = request;
    },
	
    /** @method   Widget.remove
        @readonly
        @desc     Fetches assets associated with a particular Widget type.
        @arg      {Object|jQuery} widget - Controller or element of Widget to remove.
        @see      {@link Widget.add}
        @see      {@link Widget.create}
    */
	'remove': function (widget)
	{
        log(L_FUNC, 'Widget.remove');
        
		var controller;
		
		// find controller
		if (widget instanceof jQuery)
		{
			// is jQuery object/element
			controller = widget.data('controller');
			
			// no controller - not a real widget
			if (!controller)
				return;
		}
		// is controller
		else if (widget.type && Widget.instances(widget.type).length)
			controller = widget;
		// is html element
		else if ((widget = $(widget)).length)
			controller = widget.data('controller');
		
		// unable to find controller
		if (!controller)
			return;
		
		var instanceList	= Widget.types[controller.type].instances,
			index			= instanceList.indexOf(controller);
		
		controller.onRemove && controller.onRemove();
		
		// remove from instance list		
		if (index >= 0)
		{
			delete instanceList[index];
			instanceList.splice(index, 1);
		}
		
		log(L_DEBUG, 'Removing widget', controller.type);
		
		// remove element & unbind events from $element & all child elements
		//	unbind also calls off(), eliminating on() events
		if (controller.$element && controller.$element.length)
			setTimeout(function () { controller.$element.data('controller', null).remove().off().find('*').off(); }, 0);
	}
};