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