      1 /**
      2  * @namespace
      3  */
      4 pockets.managers = {};
      5
      6 pockets.managers.EventManager = Class.create(
      7   /**
      8    * @lends pockets.managers.EventManager.prototype
      9    */
     10 {
     11   /**
     12    * @class EventManager is helper class for Mojo Scene Assistants, implementing a registry of events.  It handles registration
     13    * of both DOM events (using EventManager#register) and custom events (using EventManager#registerEventTarget).
     14    *
     15    * EventManager#startAllListening will start listening to all registered events and EventManager#stopAllListening will
     16    * stop listening to all registered Events.  These calls should be used in your scene assistant's #activate & #deactivate methods
     17    * in order to minimize memory usage/leaks.
     18    *
     19    * EventManager also tracks the Mojo.event.onStageDeactivate (app minimize) and Mojo.event.onStageActivate (app maximixe)
     20    * events and calls SceneAssistant#onStageDeactivate and SceneAssistant#onStageActivate and the EventManager will call them.
     21    * Recommended behavior is, at minimum, for a Scene Assistant to call SceneAssistant#deactivate & SceneAssistant#activate
     22    * respectively.
     23    *
     24    * TODO:
     25    *  - examples
     26    *  - explain/deprecate the takeoverActivateMethods flag and ALWAYS act as proxy for activate/deactivate/clearnup
     27    *
     28    *
     29    * By passing in a true Boolean, EventManager will take over your scene
     30    * All events registered in the EventManager will  be removed when the Scene is deactivated,
     31    *
     32    * @constructs
     33    * @param assistant {Function} a Mojo Scene Assistant
     34    * @param takeoverActivateMethods {true|false}
     35    */
     36   initialize: function(assistant, takeoverActivateMethods) {
     37     this.assistant = assistant;
     38     this.events = [];
     39     this.isActive = false;
     40     if (takeoverActivateMethods) {
     41       this.assistant.activate = this._onActivate.bind(this);
     42       this.assistant.deactivate = this._onDeactivate.bind(this);
     43       this.assistant.cleanup = this._onCleanup.bind(this);
     44     }
     45     this.stageActivate = this._onStageActivate.bind(this);
     46     this.stageDeactivate = this._onStageDeactivate.bind(this);
     47   },
     48
     49   /**
     50    * Registers an Event handler for a DOM Element. If the scene is active, the EventManager will start listening for that event.
     51    *
     52    * @param element {Element|String} DOM Element or string representing the id attribute of a DOM Element that will receive the event
     53    * @param trigger {Event} event
     54    * @param handler {Function} handler function to call when the event is caught/heard
     55    * @returns {pockets.managers.EventRegistration} the event registration, in case this needs to be unregistered
     56    */
     57   register: function(element, trigger, handler) {
     58     element = Object.isString(element) ? this.assistant.controller.get(element) : element;
     59     var registration = new pockets.managers.EventRegistration(element, trigger, handler);
     60     this.events.push(registration);
     61     if (this.isActive) {
     62       registration.listen();
     63     }
     64     return registration;
     65   },
     66
     67   /**
     68    * Registers an Event handler for a non-DOM Element class. If the scene is active, the EventManager will start listening for that event.
     69    *
     70    * @param eventTarget {pockets.EventTarget} instance of a class that will fire the event, must be a child class of pockets.EventTarget
     71    * @param trigger {Event} event
     72    * @param handler {Function} handler function to call when the event is caught/heard
     73    * @returns {pockets.managers.EventRegistration} the event registration, in case this needs to be unregistered
     74    */
     75   registerEventTarget: function(eventTarget, trigger, handler) {
     76     var registration = new pockets.managers.EventTargetRegistration(eventTarget, trigger, handler);
     77     this.events.push(registration);
     78     if (this.isActive) {
     79       registration.listen();
     80     }
     81     return registration;
     82   },
     83
     84   /**
     85    * Removes an event Registration from the collection of events. Subsequent calls to EventManager#startAllListening will not
     86    * reinstall this handler. If the scene is active, and thus the event is registered, it will stop listening.
     87    *
     88    * @param eventRegistration
     89    */
     90   unregister: function(eventRegistration) {
     91     this.events = this.events.without(eventRegistration);
     92     if (this.isActive) {
     93       eventRegistration.stopListening();
     94     }
     95   },
     96
     97   /**
     98    * Starts listening for all currently registered events.  Also starts listening for Mojo.Event.stageDeactivate.
     99    */
    100   startAllListening: function() {
    101       for (var i = 0; i < this.events.length; i++) {
    102           this.events[i].listen();
    103           }
    104       if (this.assistant.controller) {
    105           Mojo.Event.listen(this.assistant.controller.document,
    106           Mojo.Event.stageDeactivate, this.stageDeactivate);
    107           }
    108       this.isActive = true;
    109       },
    110
    111   /**
    112    * Stops listening for all currently registered events.
    113    */
    114   stopAllListening: function () {
    115     for (var i = 0; i < this.events.length; i++) {
    116       this.events[i].stopListening();
    117     }
    118     this.isActive = false;
    119   },
    120
    121   /**
    122    * Cleans up after itself.
    123    */
    124   cleanup: function() {
    125     if (this.assistant.controller) {
    126       Mojo.Event.stopListening(this.assistant.controller.document,
    127             Mojo.Event.stageDeactivate, this.stageDeactivate);
    128       Mojo.Event.stopListening(this.assistant.controller.document,
    129             Mojo.Event.stageActivate, this.stageActivate);
    130     }
    131     this.assistant = null;
    132     this.events.clear();
    133     this.events = null;
    134   },
    135
    136   /**
    137    * @ignore
    138    * @private
    139    * @param event
    140    */
    141   _onActivate: function(event) {
    142     this.startAllListening();
    143     if (this.assistant.afterActivate !== undefined) {
    144       this.assistant.afterActivate(event);
    145     }
    146   },
    147
    148   /**
    149    * @ignore
    150    * @private
    151    * @param event
    152    */
    153   _onDeactivate: function(event) {
    154     this.stopAllListening();
    155     if (this.assistant.afterDeactivate !== undefined) {
    156       this.assistant.afterDeactivate(event);
    157     }
    158   },
    159
    160   /**
    161    * @ignore
    162    * @private
    163    */
    164   _onStageActivate: function() {
    165     if (this.assistant.controller) {
    166       Mojo.Event.stopListening(this.assistant.controller.document,
    167             Mojo.Event.stageActivate, this.stageActivate);
    168       Mojo.Event.listen(this.assistant.controller.document,
    169             Mojo.Event.stageDeactivate, this.stageDeactivate);
    170     }
    171     if (this.assistant.onStageActivate) this.assistant.onStageActivate();
    172     this.startAllListening();
    173   },
    174
    175   /**
    176    * @ignore
    177    * @private
    178    */
    179   _onStageDeactivate: function() {
    180     if (this.assistant.controller) {
    181       Mojo.Event.stopListening(this.assistant.controller.document,
    182             Mojo.Event.stageDeactivate, this.stageDeactivate);
    183       Mojo.Event.listen(this.assistant.controller.document,
    184             Mojo.Event.stageActivate, this.stageActivate);
    185     }
    186     if (this.assistant.onStageDeactivate) this.assistant.onStageDeactivate();
    187     this.stopAllListening();
    188   },
    189
    190   /**
    191    * @ignore
    192    * @private
    193    * @param event
    194    */
    195   _onCleanup: function(event) {
    196     if (this.assistant.beforeCleanup !== undefined) {
    197       this.assistant.beforeCleanup(event);
    198     }
    199     this.cleanup();
    200   }
    201 });
    202
    203 pockets.managers.EventRegistration = Class.create(
    204   /**
    205    * @lends pockets.managers.EventRegistration.prototype
    206    */
    207 {
    208   /**
    209    * @class Wrapper class for tracking an individual event. Used by the EventManager, rarely used directly.
    210    *
    211    * @see pockets.managers.EventManager
    212    *
    213    * @constructs
    214    * @param element {Element|String} DOM Element or string representing the id attribute of a DOM Element that will receive the event
    215    * @param trigger {Event} event
    216    * @param handler {Function} handler function to call when the event is caught/heard
    217    */
    218   initialize: function(element, trigger, handler) {
    219     this.element = element;
    220     this.trigger = trigger;
    221     this.handler = handler;
    222   },
    223
    224   /**
    225    * Starts listening for this event
    226    */
    227   listen: function() {
    228     Mojo.Event.listen(this.element, this.trigger, this.handler);
    229   },
    230
    231   /**
    232    * Stops listening for this event
    233    */
    234   stopListening: function() {
    235     Mojo.Event.stopListening(this.element, this.trigger, this.handler);
    236   }
    237 });
    238
    239
    240 pockets.managers.EventTargetRegistration = Class.create(
    241   /**
    242    * @lends pockets.managers.EventTargetRegistration.prototype
    243    */
    244 {
    245  /** @class Wrapper class for tracking an individual event. Used by the EventManager, rarely used directly.
    246    *
    247    * @see pockets.managers.EventManager
    248    *
    249    * @constructs
    250    * @param eventTarget {pockets.EventTarget} instance of a class that will fire the event, must be a child class of pockets.EventTarget
    251    * @param trigger {Event} event
    252    * @param handler {Function} handler function to call when the event is caught/heard
    253    */
    254   initialize: function(eventTarget, trigger, handler) {
    255     this.eventTarget = eventTarget;
    256     this.trigger = trigger;
    257     this.handler = handler;
    258   },
    259
    260   /**
    261    * Starts listening for this event
    262    */
    263   listen: function() {
    264     this.eventTarget.listen(this.trigger, this.handler);
    265   },
    266
    267   /**
    268    * Stops listening for this event
    269    */
    270   stopListening: function() {
    271     this.eventTarget.unlisten(this.trigger, this.handler);
    272   }
    273 });
    274
    275 /**
    276  * <b>Disabling An App Menu Item</b>
    277  * To disable an app menu item, the item's model must contain <code>checkEnabled: true</code> and must contain
    278  * a function called <code>enabledHandler</code> which is a function that returns <code>true</code> if the item should
    279  * be enabled and <code>false</code> if not.
    280  */
    281 pockets.managers.MenuManager = Class.create(
    282   /**
    283    * @lends pockets.managers.MenuManager.prototype
    284    */
    285 {
    286   /**
    287    * @class Helper class to make it easier to work with Mojo's menus. Application, View, Command, and popup menus are supported.
    288    *
    289    * The primary value the MenuManager provides is mapping each menu item to a separate handler function - the default
    290    * behavior is to send all menu actions to an assistant's #handleCommand method (the MenuManager will pass through
    291    * to SceneAssistant#handleCommand, if present).
    292    *
    293    * Once you've separated your menu actions into separate handlers, you can test them in isolation, have the handlers be in
    294    * other classes, all the while having code that is easier to maintain.
    295    *
    296    * The MenuManager is constructed with a SceneAssistant, usually in the SceneAssistant's constructor. It holds the models
    297    * for the widgets for the menus - the SceneAssistant needs to setup the models & attributes and then call MenuManager#refreshAllMenus.
    298    *
    299    *
    300    *
    301    *
    302    * @example
    303    * // construct the MenuManager in your SceneAssistant constructor
    304    * // setup your menu models in your SceneAssistant activate method (manipulate the models directly)
    305    * // popup examples
    306    *
    307    *
    308    *
    309    * @constructs
    310    * @param assistant {Function} A Mojo Scene Assistant, ready for adding menus.
    311    */
    312   initialize: function(assistant) {
    313     this.assistant = assistant;
    314
    315     /**
    316      * Holds the widget models for the built-in widgets for the App, View and Command menus. The SceneAssistant
    317      * can manipulate these models directly.
    318      * @examples
    319      * TODO
    320      */
    321     this.models = {
    322       app: {visible: true, items: []},
    323       view: {visible: true, items: []},
    324       command: {visible: true, items: []}
    325     };
    326
    327     /**
    328      * Holds the widget attributes for the built-in widgets for the App, View and Command menus. The SceneAssistant
    329      * can manipulate these directly.
    330      */
    331     this.attributes = {
    332       app: {},
    333       view: {},
    334       command: {}
    335     };
    336
    337     this.handleCommandHandler = this.handleCommand.bind(this);
    338     this.onChooseHandler = this.onChoose.bind(this);
    339     if (this.assistant.handleCommand) {
    340       this.assistantHandleCommand = this.assistant.handleCommand.bind(this.assistant);
    341     }
    342     this.assistant.handleCommand = this.handleCommandHandler;
    343     this._hasBeenSetUp = false;
    344   },
    345
    346   /**
    347    * @ignore
    348    * @param menu
    349    * @param attributes
    350    * @param model
    351    */
    352   setupMenu: function(menu, attributes, model) {
    353     if (model.items.length > 0) {
    354       this.assistant.controller.setupWidget(menu, attributes, model);
    355     }
    356   },
    357
    358   /**
    359    *
    360    */
    361   setupAllMenus: function() {
    362     this.setupMenu(Mojo.Menu.commandMenu, this.attributes.command, this.models.command);
    363     this.setupMenu(Mojo.Menu.viewMenu, this.attributes.view, this.models.view);
    364     this.setupMenu(Mojo.Menu.appMenu, this.attributes.app, this.models.app);
    365   },
    366
    367   /**
    368    * Calls SceneController#modelChanged in order to update the Scene's App, View, and Command menu.  This will call setupWidget
    369    * on each menu that hasn't yet been setup.
    370    */
    371   refreshAllMenus: function() {
    372     if (this._hasBeenSetUp) {
    373       this.assistant.controller.modelChanged(this.models.app, this);
    374       this.assistant.controller.modelChanged(this.models.view, this);
    375       this.assistant.controller.modelChanged(this.models.command, this);
    376     } else {
    377       this.setupAllMenus();
    378       this._hasBeenSetUp = true;
    379     }
    380   },
    381
    382   /**
    383    * Directly sets the items for the Command menu & calls setupWiget.
    384    * @param items
    385    */
    386   commandMenu: function(items) {
    387     this.models.command.items = items;
    388     this.setupMenu(Mojo.Menu.commandMenu, this.attributes.command, this.models.command);
    389     return this;
    390   },
    391
    392   /**
    393    * Directly sets the items for the View menu & calls setupWiget.
    394    *
    395    * @param items
    396    */
    397   viewMenu: function(items) {
    398     this.models.view.items = items;
    399     this.setupMenu(Mojo.Menu.viewMenu, this.attributes.view, this.models.view);
    400     return this;
    401   },
    402
    403   /**
    404    * Directly sets the items for the App menu & calls setupWiget.
    405    *
    406    * @param items
    407    */
    408   appMenu: function(items) {
    409     this.models.app.items = items;
    410     this.setupMenu(Mojo.Menu.appMenu, this.attributes.app, this.models.app);
    411     return this;
    412   },
    413
    414   /**
    415    * @ignore
    416    * @param index
    417    */
    418   getCommandMenuItem: function(index) {
    419     if (pockets.nestedPropertyExists(this, "models.command.items")) {
    420       return this.models.command.items[index];
    421     }
    422   },
    423
    424   /**
    425    * @ignore
    426    *
    427    * @param index
    428    */
    429   getAppMenuItem: function(index) {
    430     if (pockets.nestedPropertyExists(this, "models.app.items")) {
    431       return this.models.app.items[index];
    432     }
    433   },
    434
    435   /**
    436    * @ignore
    437    *
    438    * @param index
    439    */
    440   getViewMenuItem: function(index) {
    441     if (pockets.nestedPropertyExists(this, "models.view.items")) {
    442       return this.models.view.items[index];
    443     }
    444   },
    445
    446   /**
    447    * Displays a popup menu with the given items.  Acts like the other menus of the MenuManager, but since it
    448    * is not a widget (like the other menus),
    449    *
    450    * @param {Element} placeNear
    451    * @param {Array} items
    452    * @param {Object} attributes
    453    * @param {Object} modelExtras
    454    */
    455   popupSubmenu: function(placeNear, items, attributes, modelExtras) {
    456     this.popupMenuModel = {placeNear: placeNear, items: items, onChoose: this.onChooseHandler};
    457     Object.extend(this.popupMenuModel, modelExtras || {});
    458     Object.extend(this.popupMenuModel, attributes || {});
    459     this.assistant.controller.popupSubmenu(this.popupMenuModel);
    460   },
    461
    462   /**
    463    *
    464    * @param handlerPropertyName
    465    * @param command
    466    * @param items
    467    */
    468   lookupHandler: function(handlerPropertyName, command, items) {
    469     var item = this.lookupItem(command, items);
    470     if (item) {
    471       return item[handlerPropertyName];
    472     } else {
    473       return null;
    474     }
    475   },
    476
    477   /**
    478    * @private
    479    * @param command
    480    * @param items
    481    */
    482   lookupItem: function(command, items) {
    483     for (var i=0; i<items.length; ++i) {
    484       var item = items[i];
    485       if (item) {
    486         if (item.items) {
    487           var foundItem = this.lookupItem(command, item.items);
    488           if (foundItem) {
    489             return foundItem;
    490           }
    491         } else if (item.command == command) {
    492           return item;
    493         }
    494       }
    495     }
    496   },
    497
    498   /**
    499    *
    500    * @param command
    501    */
    502   onChoose: function(command) {
    503     // closing the popup without selecting anything
    504     if (command == undefined) {
    505       return;
    506     }
    507
    508     var item = this.lookupItem(command, [this.popupMenuModel]);
    509     if (item && item.commandHandler) {
    510       item.commandHandler(command);
    511     } else {
    512       Mojo.Log.error("**** Got strange popup command " + command + " ****");
    513     }
    514   },
    515
    516   /**
    517    *
    518    * @param event
    519    */
    520   handleCommand: function(event) {
    521     var item = this.lookupItem(event.command, [this.models.app, this.models.view, this.models.command]);
    522     if (item) {
    523       if (event.type == Mojo.Event.command && item.commandHandler) {
    524         event.stop();
    525         item.commandHandler(event);
    526         return;
    527       } else if (event.type == Mojo.Event.commandEnable) {
    528         if (item.enabledHandler) {
    529           if (!item.enabledHandler()) {
    530             event.preventDefault();
    531           }
    532           return;
    533         } else {
    534           if (item.disabled) {
    535             event.preventDefault();
    536           }
    537           return;
    538         }
    539       }
    540     } else {
    541       if (event.command == Mojo.Menu.helpCmd) {
    542         if (event.type == Mojo.Event.commandEnable && this.helpHandler) {
    543           event.stopPropagation();
    544         }
    545         if (event.type == Mojo.Event.command && this.helpHandler) {
    546           this.helpHandler();
    547         }
    548       }
    549     }
    550
    551     if (this.assistantHandleCommand) {
    552       this.assistantHandleCommand(event);
    553     }
    554   }
    555 });
    556
    557