SOURCE CODE: Uize.Widget.mEventBindings

VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Widget.mEventBindings Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://uize.com
| /____/ /__/_| | COPYRIGHT : (c)2014 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 7
  codeCompleteness: 100
  docCompleteness: 10
*/

/*?
  Introduction
    The =Uize.Widget.mEventBindings= mixin implements features to provide a declarative approach to wiring Uize events on widgets and their children, as well as DOM events on implied nodes.

    *DEVELOPERS:* `Ben Ilegbodu`, original code contributed by `Zazzle Inc.`
*/

Uize.module ({
  name:'Uize.Widget.mEventBindings',
  builder:function () {
    'use strict';

    var
      /*** Variables for Scruncher Optimization ***/
        _Uize = Uize,
        _forEach = _Uize.forEach,
        _returnTrue = _Uize.returnTrue,
        _isFunction = _Uize.isFunction,
        _isPlainObject = _Uize.isPlainObject,
        _pairUp = _Uize.pairUp
    ;

    return function (_class) {
      _class.declare ({
        alphastructor:function () {
          var
            m = this,
            _mClass = m.Class,
            _domEventsLookup = _mClass.mEventBindings_dom,
            _addedChildren = m.addedChildren,
            _children = m.children,
            _wiredWidgetEvents = {} // keep track of wired widget events so we can remove them if the children get removed
          ;

          function _wrapBinding(_context, _binding, _source, _defaultFireIf) {
            return function(_event) {
              // ensure that the required child widgets have been added before calling the handler
              // or that the fireIf condition has been met
              (!_binding.required || _addedChildren.isMet(_binding.required))
                && m.isMet(_binding.fireIf || _defaultFireIf)
                && _binding.handler.call(_context, _event, _source)
              ;
            };
          }

          /* NOTE: format of bindings
            {
              sourceName:[
                [
                  eventName,
                  {
                    handler:function() { },
                    required:'',
                    fireIf:''
                  }
                ]
              ]
            }
          */

          /*** wire up event handlers for DOM nodes ***/
            m.whenever (
              'wired',
              function () {
                for (var _nodeName in _domEventsLookup) {
                  for (
                    var
                      _bindings = _domEventsLookup[_nodeName],
                      _node = m.getNode(_nodeName),
                      _bindingNo = -1,
                      _bindingsLength = _bindings.length
                    ;
                    ++_bindingNo < _bindingsLength;
                  ) {
                    var _bindingInfo = _bindings[_bindingNo];
                    m.wireNode(_node, _bindingInfo[0], _wrapBinding(m, _bindingInfo[1], _node, 'enabledInherited,!busyInherited'));
                  }
                }
              }
            );

          /*** wire self & child widget events ***/
            _forEach(
              _mClass.mEventBindings_widget,
              function(_bindings, _widgetName) {
                function _wire(_widget) {
                  for (
                    var
                      _bindingNo = -1,
                      _bindingsLength = _bindings.length
                    ;
                    ++_bindingNo < _bindingsLength;
                  ) {
                    var
                      _bindingInfo = _bindings[_bindingNo],
                      _eventToWire = _pairUp(_bindingInfo[0], _wrapBinding(m, _bindingInfo[1], _widget, _returnTrue))
                    ;

                    _widget.wire(_eventToWire);

                    // store a reference to the wired event for later
                    (_wiredWidgetEvents[_widgetName] || (_wiredWidgetEvents[_widgetName] = [])).push(_eventToWire);
                  }
                }
                if (_widgetName) {
                  var _childWidget;
                  _addedChildren.wire( // adding or removing child widgets
                    'Changed.' + _widgetName,
                    function() {
                      if (_addedChildren.get(_widgetName))
                        _wire(_childWidget = _children[_widgetName]);
                      else {
                        // unwire all the events this widget wired so that we don't have a removed child widget still potentially
                        // firing events
                        for (var _wiredEventForWidget = _wiredWidgetEvents[_widgetName], _eventNo = -1; ++_eventNo < _wiredEventForWidget.length;)
                          _childWidget.unwire(_wiredEventForWidget[_eventNo]);

                        // delete our cache of the wired event so things don't get mixed up when if we
                        // add back a child w/ the same name
                        delete _wiredWidgetEvents[_widgetName];

                        // clear out our reference to the removed child widget to not potentially hang on memory that can be disposed
                        _childWidget = undefined;
                      }
                    }
                  );
                }
                else _wire(m); // '' is self
              }
            );
        },

        staticMethods:{
          eventBindings:function(_bindings) {
            var
              _Class = this,
              _domEventBindings = _Class.mEventBindings_dom,
              _widgetEventBindings = _Class.mEventBindings_widget,
              _undefined
            ;

            _forEach(
              _bindings,
              function(_eventBindingValue, _eventBindingKey) {
                var
                  _eventBindingKeyTokens = _eventBindingKey.split(':'), // NOTE: widget events with colons won't work as a result
                  _sourceName = _eventBindingKeyTokens[0],
                  _eventBindings = (!_sourceName.indexOf('#') ? (_sourceName = _sourceName.substr(1)) : _undefined) != _undefined // DOM references start with #
                    ? _domEventBindings
                    : _widgetEventBindings
                ;

                function _addBinding(_binding, _eventName) {
                  if (!_sourceName && _eventBindings == _widgetEventBindings && _isFunction(_binding) && !_eventName.indexOf('Changed.')) {
                    var _propertyName = _eventName.slice(8);
                    // For optimization purposes, we want to add a self widget Changed.* handler for the onChange property
                    // of the property name
                    _Class.stateProperties(
                      _pairUp(
                        _propertyName,
                        {
                          onChange:function() {
                            var m = this;
                            _binding.call(
                              m,
                              {
                                name:_eventName,
                                source:m,
                                newValue:m.get(_propertyName)
                              },
                              m
                            );
                          }
                        }
                      )
                    );
                  }
                  else
                    // push binding info to an array instead of adding to an object (with event name as key) so that subclasses can also wire
                    // up the same event for the same child without overriding
                    (_eventBindings[_sourceName] || (_eventBindings[_sourceName] = [])).push([_eventName, _isPlainObject(_binding) ? _binding : {handler:_binding}])
                  ;
                }

                _eventBindingKeyTokens.length > 1 // short-hand syntax where the 2nd token is the event
                  ? _addBinding(_eventBindingValue, _eventBindingKeyTokens[1])
                  : _forEach(_eventBindingValue, _addBinding)
                ;
              }
            );
            /*?
              Static Methods
                Uize.Widget.mEventBindings.eventBindings
                  .

                  SYNTAX
                  .........................................
                  MyWidgetClass.eventBindings (bindingsOBJ);
                  .........................................

                  VERBOSE EXAMPLE
                  ......................................................
                  MyNamespace.MyWidgetClass = Uize.Widget.mEventBindings.subclass ({
                    eventBindings:{
                      '#menu':{ // DOM node
                        click:function (event, sourceNode) { },
                        mouseover:{
                          handler:function(_event, sourceNode) { },
                          fireIf:'busyInherited'
                        }
                      },
                      'sliderG':{ // child widget
                        'Changed.value':function (event, sourceWidget) { },
                        Update:function(_event, sourceWidget) { }
                      },
                      '':{ // self
                        'Changed.value':function (event) { },
                        Update:{
                          handler:function(_event) { },
                          required:'sliderG'
                        }
                      }
                    }
                  });
                  ......................................................

                  SHORT-HAND EXAMPLE
                  ......................................................
                  MyNamespace.MyWidgetClass = Uize.Widget.mEventBindings.subclass ({
                    eventBindings:{
                      '#menu:click':function(event, sourceNode) { },
                      '#menu:mouseover':{
                        handler:function(event, sourceNode) { },
                        fireIf:'busyInherited'
                      },
                      'sliderG:Changed.value':function (_event, sourceWidget) { },
                      'sliderG:Update':function(event, sourceWidget) { },
                      ':Changed.value':function (event) { },
                      ':Update':{
                        handler:function(event) { },
                        required:['sliderG']
                      }
                    }
                  });
                  ......................................................
            */
          }
        },

        staticProperties:{
          mEventBindings_dom:{},
          mEventBindings_widget:{}
        }
      });
    };
  }
});