# The EventManager is a proxy between the DOM-level # events and the higher-lever GUI API's. EventManagerApp = HApplication.extend # # Warning wrapper, only active in development mode warn: -> console.warn.apply( console, arguments ) unless @isProduction # # The default options array, each flag corresponds to events received. _defaultEventOptions: # # The recipient's #mouseMove(x,y) method gets called, # whether it has focus or not mouseMove: false # # The recipient's #mouseDown(x,y,button) method gets called, # if focused. mouseDown: false # # The recipient's #click(x,y,button) method gets called, # if focused. click: false # # The recipient's #mouseUp(x,y,button) method gets called, # if focused. mouseUp: false # # The recipient's #startDrag( x, y ), #drag( x, y ) and # #endDrag( x, y ) methods get called, if focused and # a draging operation is done with the pointing device. draggable: false # # The recipient's #startHover( dragObj ), #hover( dragObj ) and # #endHover( dragObj ) methods get called, when a dragged object # is hovered over it. See the rectHover option to set the topmost # vs any mode. droppable: false # # The drop modes define how the hover area responds to dragged objects # false: point; the [x,y] point of the pointer # true: area; the [l,t,r,b] contains rectangle of the dragged object # 'contains': for areas; same as true # 'intersects': for areas; intersection instead of containment rectHover: false # # Allows multiple (other than the non-topmost drop target). # Set on both the draggable as well as the drop target object. multiDrop: false # # The recipient's #keyDown( keyCode ) method gets called, # if active and a keyboard key is pressed. keyDown: false # # The recipient's #keyUp( keyCode ) method gets called, # if active and a keyboard key is released. keyUp: false # # The recipient's #mouseWheel( delta) method gets called, # if active and a scroll device is used. mouseWheel: false # # The recipient's #resize() method gets called, when the # browser's window or a HWindow (and similar components) are # resized, this event is triggered on all recipients. resize: false # # The recipient's #textEnter() method gets called, when a keyboard key # has been pressed and released, whether it has focus or not. textEnter: false # # The recipient's #doubleClick(x,y) method gets called, when # the pointing device is double-clicked, if focused. doubleClick: false # # The recipient's #contextMenu() method gets called, when the # pointing device's or keyboard's context button (usually the # right mouse button) has been pressed, if focused. contextMenu: false # # The status object has a list of the most recent HID events by id and name. # Each id and name and states are booleans. # button1 => The state of the left mouse button. # button2 => The state of the right mouse button. # crsrX => The X-coordinate of the mouse cursor (or last point of touch). # crsrY => The Y-coordinate of the mouse cursor (or last point of touch). # keysDown => An Array of the key codes of keys being held down. # altKeyDown => The state of the Alt key (or equivalent, like Option). # ctrlKeyDown => The state of the Ctrl key. # shiftKeyDown => The state of the Shift key. # metaKeyDown => The state of the Meta key. # cmdKeyDown => The state of the Command key (or equivalent, like the Windows key). # Note: Will be deprecated in favor of an hash with keys instead of numeric indexes. status: 0: false button1: false setButton1: (f)-> @button1 = f @[0] = f 1: false button2: false setButton2: (f)-> @button2 = f @[1] = f 2: -1 crsrX: -1 setCrsrX: (x)-> @crsr[0] = x @crsrX = x @[2] = x 3: -1 crsrY: -1 crsr: [ -1, -1 ] setCrsrY: (y)-> @crsr[1] = y @crsrY = y @[3] = y setCrsr: (x,y)-> @setCrsrX(x) @setCrsrY(y) 4: [] keysDown: [] hasKeyDown: (k)-> !!~@keysDown.indexOf(k) addKeyDown: (k)-> if !@hasKeyDown(k) @keysDown.push( k ) @[4] = @keysDown delKeyDown: (k)-> i = @keysDown.indexOf(k) if ~i @keysDown.splice( i, 1 ) @[4] = @keysDown resetKeysDown: -> @keysDown = [] @[4] = @keysDown 5: false altKeyDown: false setAltKey: (f)-> @altKeyDown = f @[5] = f 6: false ctrlKeyDown: false setCtrlKey: (f)-> @ctrlKeyDown = f @[6] = f 7: false shiftKeyDown: false setShiftKey: (f)-> @shiftKeyDown = f @[7] = f 8: false metaKeyDown: false setMetaKey: (f)-> @metaKeyDown = f @[8] = f 9: false cmdKeyDown: false setCmdKey: (f)-> @cmdKeyDown = f @[9] = f length: 10 button1: 0 button2: 1 crsrX: 2 crsrY: 3 keysDown: 4 altKeyDown: 5 ctrlKeyDown: 6 shiftKeyDown: 7 metaKeyDown: 8 cmdKeyDown: 9 # # Cleans up events that would be lost, when the browser window is blurred. _domWindowBlur: -> @status.resetKeysDown() @status.setAltKey(false) @status.setCtrlKey(false) @status.setShiftKey(false) @status.setMetaKey(false) @status.setCmdKey(false) @_origDroppableChecks = @enableDroppableChecks @enableDroppableChecks = false if HSystem.defaultInterval != HSystem._blurredInterval @_sysDefaultInterval = HSystem.defaultInterval COMM.Queue.push( (-> HSystem.defaultInterval = HSystem._blurredInterval ) ) # # Restores system poll frequency _domWindowFocus: -> @enableDroppableChecks = @_origDroppableChecks if HSystem.defaultInterval == HSystem._blurredInterval and @_sysDefaultInterval _this = @ COMM.Queue.push( (-> HSystem.defaultInterval = _this._sysDefaultInterval ) ) # # List of used event methods for global purposes: _eventMethods: [ 'resize', 'mouseMove', 'doubleClick', 'contextMenu', 'click', 'mouseUp', 'mouseDown', 'keyUp', 'keyDown', 'mouseWheel' ] # # This structure keeps track of registered elem/event/object/method; see #observe and #stopObserving _observerMethods: {} # # Observe event, cache anon function; eventName => elem => anonFn observe: (_elem, _eventName, _targetMethodName, _targetObj, _capture)-> _targetObj = @ unless _targetObj? _capture = false unless _capture? _anonFn = (e)-> _targetObj[_targetMethodName](e) unless @_observerMethods[_eventName]? @_observerMethods[_eventName] = { elems: [] fns: [] capture: [] } _cache = @_observerMethods[_eventName] _elemIdx = _cache.elems.indexOf( _elem ) if ~_elemIdx _prevFn = _cache.fns[_elemIdx] _prevCapture = _cache.capture[_elemIdx] Event.stopObserving( _elem, _eventName, _prevFn, _prevCapture ) _cache.elems.splice( _elemIdx, 1 ) _cache.fns.splice( _elemIdx, 1 ) _cache.capture.splice( _elemIdx, 1 ) Event.observe( _elem, _eventName, _anonFn, _capture ) _cache.elems.unshift( _elem ) _cache.fns.unshift( _anonFn ) _cache.capture.unshift( _capture ) true # # Stop observing event stopObserving: (_elem, _eventName)-> return false unless @_observerMethods[_eventName]? if @_observerMethods[_eventName].elems.length == 0 delete @_observerMethods[_eventName] return false _cache = @_observerMethods[_eventName] _elemIdx = _cache.elems.indexOf(_elem) return false unless ~_elemIdx _prevFn = _cache.fns[_elemIdx] _prevCapture = _cache.capture[_elemIdx] Event.stopObserving( _elem, _eventName, _prevFn, _prevCapture ) _cache.elems.splice( _elemIdx, 1 ) _cache.fns.splice( _elemIdx, 1 ) _cache.capture.splice( _elemIdx, 1 ) return true # # This structure keeps track of view-event bindings: _listeners: { byId: {} # viewId => [ eventName, eventName, .. ] _rectHoverIntersectMode: [] byEvent: # event names by viewId mouseMove: [] # viewIds mouseDown: [] # viewIds mouseUp: [] # viewIds draggable: [] # viewIds droppable: [] # viewIds rectHover: [] # viewIds multiDrop: [] # viewIds keyRepeat: [] # viewIds keyDown: [] # viewIds keyUp: [] # viewIds mouseWheel: [] # viewIds textEnter: [] # viewIds click: [] # viewIds resize: [] # viewIds doubleClick: [] # viewIds contextMenu: [] # viewIds focused: [] # viewIds enabled: [] # viewIds dragged: [] # viewIds selected: [] # viewIds hovered: [] # viewIds active: [] # viewIds } # # Returns the global target element based on browser type _getGlobalTargetElem: -> if BROWSER_TYPE.ie and !BROWSER_TYPE.ie9 return window.document else return window # # Starts EventManager start: -> @_views = HSystem.views # shortcut to system views _targetElem = @_getGlobalTargetElem() _methods = @_eventMethods _methods.push( 'keyPress' ) unless BROWSER_TYPE.safari or BROWSER_TYPE.ie for _methodName in _methods if _methodName == 'doubleClick' _eventName = 'dblclick' else _eventName = _methodName.toLowerCase() @observe( _targetElem, _eventName, _methodName ) if window.addEventListener? @observe( window, 'DOMMouseScroll', 'mouseWheel' ) @observe( window, 'resize', 'resize' ) @observe( window, 'blur', '_domWindowBlur' ) @observe( window, 'focus', '_domWindowFocus' ) # # Stops EventManager stop: -> delete @_views _targetElem = @_getGlobalTargetElem() _methods = @_eventMethods _methods.push( 'keyPress' ) unless BROWSER_TYPE.safari or BROWSER_TYPE.ie for _methodName in _methods if _methodName == 'doubleClick' _eventName = 'dblclick' else _eventName = _methodName.toLowerCase() @stopObserving( _targetElem, _eventName, _methodName ) if window.addEventListener? @stopObserving( window, 'DOMMouseScroll', 'mouseWheel' ) @stopObserving( window, 'resize', 'resize' ) @stopObserving( window, 'blur', '_domWindowBlur' ) @stopObserving( window, 'focus', '_domWindowFocus' ) # # Ensures the type of the object is a HControl _ensureValidControl: (_ctrl,_warnMethodName)-> _warnMethodName = '[unknown]' unless _warnMethodName unless _ctrl.hasAncestor? @warn( "EventManager##{_warnMethodName} => Not a HClass: ", _ctrl ) return false unless _ctrl.hasAncestor( HView ) and _ctrl.isCtrl @warn( "EventManager##{_warnMethodName} => Not a HControl: ", _ctrl ) return false return true # # Ensure valid eventOptions _ensureValidEventOptions: (_eventOptions,_warnMethodName)-> _warnMethodName = '[unknown]' unless _warnMethodName if !( typeof _eventOptions == 'object' ) @warn( "EventManager##{_warnMethodName} => Invalid eventOptions: ", _eventOptions ) return false return true # # Queue of items to check for focus, because it's fairly slow directly: _topmostQueue: [] # # Converts eventOptions into a list of enabled event names _setEventOptions: (_ctrl,_eventOptions,_warnMethodName)-> _warnMethodName = '[unknown]' unless _warnMethodName _viewId = _ctrl.viewId _eventsOn = [] for _key of @_defaultEventOptions continue if _key == 'base' or _key == 'constructor' if @_listeners.byEvent[_key]? and _eventOptions[_key]? _value = _eventOptions[_key] if _value _eventsOn.push( _key ) if _key == 'keyDown' if _value == 'repeat' unless ~@_listeners.byEvent['keyRepeat'].indexOf(_viewId) @_listeners.byEvent['keyRepeat'].unshift( _viewId ) else _idx = @_listeners.byEvent['keyRepeat'].indexOf(_viewId) @_listeners.byEvent['keyRepeat'].splice( _idx, 1 ) if ~_idx unless ~@_listeners.byEvent[_key].indexOf(_viewId) @_listeners.byEvent[_key].unshift( _viewId ) if _key == 'rectHover' and _value == 'intersects' and not ~@_listeners._rectHoverIntersectMode.indexOf( _viewId ) @_listeners._rectHoverIntersectMode.unshift( _viewId ) else # not _value _keys = [_key] _keys.push('keyRepeat') if _key == 'keyDown' for _key in _keys _idx = @_listeners.byEvent[_key].indexOf(_viewId) @_listeners.byEvent[_key].splice( _idx, 1 ) if ~_idx else @warn( "EventManager##{_warnMethodName} => Invalid event type: #{_key}" ) _this = @ @_listeners.byId[_viewId] = _eventsOn if _ctrl.enabled @_listeners.enabled.unshift( _viewId ) unless ~@_listeners.enabled.indexOf(_viewId) _elem = ELEM.get( _ctrl.elemId ) [ x, y ] = @status.crsr @_topmostQueue.push( [ HPoint.new( x, y ), _ctrl ] ) # # Releases bindings done by #_setEventOptions _unsetEventOptions: (_ctrl,_warnMethodName)-> _warnMethodName = '[unknown]' unless _warnMethodName _viewId = _ctrl.viewId if @_listeners.byId[_viewId]? delete @_listeners.byId[_viewId] else @warn( "EventManager##{_warnMethodName} => viewId not registered: #{_viewId}" ) for _key, _value of @_listeners.byEvent _viewIdx = _value.indexOf(_viewId) _value.splice(_viewIdx,1) if ~_viewIdx if _key == 'rectHover' _intersectHoverIdx = @_listeners._rectHoverIntersectMode.indexOf( _viewId ) if ~_intersectHoverIdx @_listeners._rectHoverIntersectMode.splice( _intersectHoverIdx, 1 ) _elem = ELEM.get( _ctrl.elemId ) for _statusItem in [ 'dragged', 'selected', 'hovered', 'active', 'focused', 'enabled' ] _viewIdx = @_listeners[_statusItem].indexOf(_viewId) if ~_viewIdx if _statusItem == 'dragged' _ctrl.endDrag( @status.crsrX, @status.crsrY ) else if _statusItem == 'selected' _ctrl.deSelect() else if _statusItem == 'hovered' _ctrl.endHover( null ) else if _statusItem == 'active' _ctrl._lostActiveStatus( null ) else if _statusItem == 'focused' @blur( _ctrl ) else if _statusItem == 'enabled' _ctrl.setEnabled( false ) if _ctrl.enabled _viewIdx = @_listeners[_statusItem].indexOf(_viewId) if ~_viewIdx @_listeners[_statusItem].splice(_viewIdx,1) # # Cancels the drag operation for the ctrl cancelDrag: (_ctrl)-> _dragIdx = @_listeners.dragged.indexOf(_ctrl.viewId) if ~_dragIdx @_listeners.dragged.splice( _dragIdx, 1 ) # # Registers the HControl -derived object _ctrl by event listener flags # in _eventOptions. reg: (_ctrl, _eventOptions)-> return false unless @_ensureValidControl( _ctrl, 'reg' ) return false unless @_ensureValidEventOptions( _eventOptions, 'reg' ) @_setEventOptions( _ctrl, _eventOptions, 'reg' ) return true # # Returns status of registration _isreg: (_ctrl)-> return @_listeners.byId[_ctrl.viewId]? # # Unregisters the HControl -derived object from all its bindings unreg: (_ctrl)-> return false unless @_ensureValidControl( _ctrl, 'unreg' ) return false unless @_isreg( _ctrl ) @_unsetEventOptions(_ctrl,'unreg') # # ## Event responders; mid-level handlers. ## These all receive the global events and check where to delegate them. # # Handle event modifiers _modifiers: (e)-> [ _x, _y ] = ELEM.getScrollPosition(0) [ x, y ] = [ Event.pointerX(e) - _x, Event.pointerY(e) - _y ] unless isNaN(x) or isNaN(y) @status.setCrsr( x, y ) @status.setAltKey( e.altKey ) @status.setCtrlKey( e.ctrlKey ) @status.setShiftKey( e.shiftKey ) @status.setMetaKey( e.metaKey ) # # Resize is an event triggered by resizing the browser window # (as well as the HWindow component, as a special case) # # The HSystem._updateFlexibleRects call may not be neccessary to call # both before and after in all circumstances, but better be safe than sure. resize: (e)-> HSystem._updateFlexibleRects() ELEM.flush() for _viewId in @_listeners.byEvent.resize _ctrl = @_views[_viewId] _ctrl.resize() if _ctrl.resize? setTimeout(-> ELEM.flush() HSystem._updateFlexibleRects() , 100 ) # # Finds the next elem with a view_id attribute _findViewId: (_elem)-> until _elem.view_id? or _elem == document.body _elem = _elem.parentNode return _elem # # Finds the ctrl based on the element by getting the view_id attribute _findEventControl: (e,_warnMethodName,_stop)-> _warnMethodName = '[unknown]' unless _warnMethodName _elem = Event.element(e) _ctrl = null until _ctrl _elem = @_findViewId( _elem ) return false if _elem == document.body unless _elem.view_id? @warn( "EventManager##{_warnMethodName} => The element doesn't have an 'view_id' attribute.") return false _viewId = _elem.view_id unless @_views[_viewId]? @warn( "EventManager##{_warnMethodName} => The viewId:#{_viewId} doesn't have a view.") return false _ctrl = @_views[_viewId] unless _ctrl.isCtrl #@_ensureValidControl(_ctrl,_warnMethodName) _ctrl = null _elem = _elem.parentNode Event.stop(e) if _stop return _ctrl # # Focuses a control, triggered based on the view-element-specific # mouseover event. focus: (_ctrl)-> _viewId = _ctrl.viewId unless ~@_listeners.focused.indexOf(_viewId) _elem = ELEM.get( _ctrl.elemId ) @_listeners.focused.unshift(_viewId) unless _ctrl.focus? @warn( "EventManager#focus => The viewId:#{_viewId} doesn't have a 'focus' method.") return false _ctrl.focus() # # Blurs a control, triggered based on the view-element-specific # mouseout event. blur: (_ctrl)-> _viewId = _ctrl.viewId _viewIdx = @_listeners.focused.indexOf(_viewId) if ~_viewIdx _elem = ELEM.get( _ctrl.elemId ) @_listeners.focused.splice(_viewIdx,1) unless _ctrl.blur? @warn( "EventManager#blur => The viewId:#{_viewId} doesn't have a 'blur' method.") return false _ctrl.blur() _debugHighlight: (_ctrl)-> unless @isProduction unless @_debugHighlightT? @_debugHighlightT = ELEM.make(0,'div') @_debugHighlightR = ELEM.make(0,'div') @_debugHighlightB = ELEM.make(0,'div') @_debugHighlightL = ELEM.make(0,'div') for _elemId in [ @_debugHighlightT, @_debugHighlightL, @_debugHighlightB, @_debugHighlightR ] ELEM.setStyles( _elemId, position: 'absolute' left: 0 top: 0 width: 0 height: 0 background: 'red' zIndex: 20000 ) for _elemId in [ @_debugHighlightT, @_debugHighlightB ] ELEM.setStyle( _elemId, 'height', '1px' ) for _elemId in [ @_debugHighlightL, @_debugHighlightR ] ELEM.setStyle( _elemId, 'width', '1px' ) _rect = _ctrl.rect x = _ctrl.pageX() y = _ctrl.pageY() w = _rect.width h = _rect.height for _elemId in [ @_debugHighlightT, @_debugHighlightL ] ELEM.setStyle( _elemId, 'left', x+'px' ) ELEM.setStyle( _elemId, 'top', y+'px' ) for _elemId in [ @_debugHighlightT, @_debugHighlightB ] ELEM.setStyle( _elemId, 'width', w+'px' ) for _elemId in [ @_debugHighlightL, @_debugHighlightR ] ELEM.setStyle( _elemId, 'height', h+'px' ) ELEM.setStyle( @_debugHighlightB, 'top', (y+h)+'px' ) ELEM.setStyle( @_debugHighlightB, 'left', x+'px' ) ELEM.setStyle( @_debugHighlightR, 'top', y+'px' ) ELEM.setStyle( @_debugHighlightR, 'left', (x+w)+'px' ) # # Mouse movement manager. Triggered by the global mousemove event. # Delegates the following event responder methods to focused HControl instances: # - drag # - mouseMove # - endHover # - startHover mouseMove: (e)-> @_modifiers(e) # fetch event modifiers [ x, y ] = @status.crsr Event.stop(e) if @_handleMouseMove( x, y ) # # Finds new focusable components after the # mouse has been moved (replacement of mouseover/mouseout) _findNewFocus: (x,y)-> _dragged = @_listeners.dragged # return if _dragged.length != 0 _matchIds = @_findTopmostEnabled( HPoint.new( x, y ), 'contains', null ) _focused = @_listeners.focused if _matchIds.length == 0 # blur all previously focused for _focusId in _focused _ctrl = @_views[_focusId] if _ctrl? @blur( _ctrl ) _focused.splice( _focused.indexOf(_focusId), 1 ) for _viewId in _matchIds continue if ~_focused.indexOf(_viewId) _ctrl = @_views[_viewId] for _focusId in _focused _focusCtrl = @_views[_focusId] if _focusCtrl? @blur( _focusCtrl ) _focused.splice( _focused.indexOf(_focusId), 1 ) @_debugHighlight(_ctrl) @focus( _ctrl ) # # Just split to gain namespace: _handleMouseMove: ( x, y )-> @_findNewFocus(x,y) @_delegateMouseMove(x,y) _currentlyDragging = @_delegateDrag(x,y) return _currentlyDragging # # Handle items being dragged, sending #drag(x,y) calls to them _delegateDrag: (x, y)-> _dragItems = @_listeners.dragged return false if _dragItems.length == 0 for _viewId in _dragItems _ctrl = @_views[_viewId] _ctrl.drag(x,y) @_delegateHover( _ctrl, x, y ) return true # # Handle items wanting #mouseMove(x,y) calls _delegateMouseMove: (x,y)-> _mouseMoveItems = @_listeners.byEvent.mouseMove return false if _mouseMoveItems.length == 0 for _viewId in _mouseMoveItems _ctrl = @_views[_viewId] _ctrl.mouseMove(x,y) return true # # Handle items wanting #startHover( _dragObj ), # #hover( _dragObj ) and #endHover( _dragObj ) calls _delegateHover: (_ctrl, x, y)-> _byEvent = @_listeners.byEvent _findPoint = not ~_byEvent.rectHover.indexOf( _ctrl.viewId ) _findTopmost = not ~_byEvent.multiDrop.indexOf( _ctrl.viewId ) _findIntersect = true if _findPoint _area = HPoint.new( x, y ) _matchMethod = 'contains' else _rect = _ctrl.rect _area = HRect.new( x, y, _rect.width, _rect.height ) if not ~@_listeners._rectHoverIntersectMode.indexOf( _viewId ) _matchMethod = 'intersects' else _matchMethod = 'contains' if _findTopmost _hoverItems = @_findTopmostDroppable(_area,_matchMethod,_ctrl.viewId) else _hoverItems = @_findAllDroppable(_area,_matchMethod,_ctrl.viewid) _endHover = [] for _hoverId in @_listeners.hovered unless ~_hoverItems.indexOf( _hoverId ) _endHover.push( _hoverId ) _startHover = [] for _hoverId in _hoverItems unless ~@_listeners.hovered.indexOf( _hoverId ) _startHover.push( _hoverId ) for _viewId in _startHover @_views[_viewId].startHover(_ctrl) for _viewId in _endHover @_views[_viewId].endHover(_ctrl) for _viewId in _hoverItems @_views[_viewId].hover(_ctrl) @_listeners.hovered = _hoverItems # # Finds the topmost item from array of viewId's _findTopmostOf: (_arrOfIds, _area, _matchMethod, _selfId )-> _views = @_views _search = (_parentIds)-> for _viewId in _parentIds continue if _viewId == _selfId _view = _views[_viewId] if _view.parent.hasAncestor( HView ) _parent = _view.parent if _parent.markupElemIds? and _parent.markupElemIds.subview? _subviewId = _parent.markupElemIds.subview [ _subX, _subY ] = ELEM.getPosition( _subviewId ) else [ _subX, _subY ] = [ 0, 0 ] if _area.offsetTo? # is a rectangle _searchArea = HRect.new(_area).offsetTo( _area.left-_parent.pageX()-_subX, _area.top-_parent.pageY()-_subY ) else # is a point _searchArea = HPoint.new( _area.x-_parent.pageX()-_subX, _area.y-_parent.pageY()-_subY ) else if _view.parent.hasAncestor( HApplication ) _searchArea = _area else console.warn('invalid view parent:',_view.parent) continue if _view.hasAncestor? and _view.hasAncestor( HView ) if _view.rect[_matchMethod](_searchArea) and !_view.isHidden if ~_arrOfIds.indexOf( _viewId ) _foundId = _search( _view.viewsZOrder.slice().reverse() ) return _foundId unless _foundId == false return _viewId else _result = _search( _view.viewsZOrder.slice().reverse() ) return _result unless _result == false return false _foundId = _search( HSystem.viewsZOrder.slice().reverse() ) return [ _foundId ] unless _foundId == false return [] # # Finds the topmost drop/hover target within the area specified by rectHover _findTopmostDroppable: (_area,_matchMethod,_selfId)-> return @_findTopmostOf( @_listeners.byEvent.droppable, _area, _matchMethod, _selfId ) # # Finds the topmost enabled target within the area specified by area _findTopmostEnabled: (_area,_matchMethod,_selfId)-> return @_findTopmostOf( @_listeners.enabled, _area, _matchMethod, _selfId ) # # Finds all drop/hover targets within the area specified by rectHover _findAllDroppable: (_area,_matchMethod,_selfId)-> _views = @_views _droppable = @_listeners.byEvent.droppable _found = [] _search = (_parentIds)-> for _viewId in _parentIds continue if _viewId == _selfId _view = _views[_viewId] if _view.hasAncestor? and _view.hasAncestor( HView ) and _view.rect[_matchMethod](_area) _found.push( _viewId ) if ~_droppable.indexOf( _viewId ) _search( _view.viewsZOrder.slice().reverse() ) _search( HSystem.viewsZOrder.slice().reverse() ) return _found # # Removes the active control delActiveControl: (_newActive)-> _active = @_listeners.active _focused = @_listeners.focused _dragged = @_listeners.dragged _hovered = @_listeners.hovered return null if _active.length == 0 _prevActive = @_views[_active[0]] if _active.length != 1 @warn "Danger, too many active items: #{JSON.stringify(_active)}" for _viewId in _active.slice() if _viewId == null console.warn('encountered null viewId in active listeners!') _idx = _active.indexOf( _viewId ) while ~_idx _idx = _active.indexOf( _viewId ) _active.splice( _idx, 1 ) continue continue if _newActive != null and _viewId == _newActive.viewId _ctrl = @_views[_viewId] if _ctrl.isDead and !@isProduction console.warn('trying to deactivate dead control!') _ctrl.active = false _idx = _active.indexOf( _viewId ) _dragIdx = _dragged.indexOf(_viewId) if ~_dragIdx _dragged.splice( _dragIdx, 1 ) for _dropViewId in _hovered _dropCtrl = @_views[_dropViewId] _dropCtrl.endHover(_ctrl) if _dropCtrl.endHover? _dropCtrl.drop(_ctrl) if _dropCtrl.drop? [ x, y ] = @status.crsr _ctrl.endDrag( x, y ) _active.splice( _idx, 1 ) @blur(_ctrl) if ~_focused.indexOf(_viewId) and _ctrl? _ctrl.lostActiveStatus(_newActive) _prevActive # # Adds the active control addActiveControl: (_ctrl,_prevActive)-> _active = @_listeners.active _focused = @_listeners.focused _idx = _active.indexOf( _ctrl.viewId ) unless ~_idx _active.unshift(_ctrl.viewId) @focus(_ctrl) unless ~_focused.indexOf(_ctrl.viewId) _ctrl.active = true _ctrl.gainedActiveStatus(_prevActive) # # Sets the active control changeActiveControl: (_ctrl)-> return if _ctrl != null and @_views[@_listeners.active[0]] == _ctrl _prevActive = @delActiveControl(_ctrl) @addActiveControl(_ctrl, _prevActive) if _ctrl != null # # Method to be called, when you want to make an item draggable from outside of the EventManager startDragging: (_ctrl)-> return if !_ctrl.enabled _viewId = _ctrl.viewId @focus( _viewId ) @changeActiveControl(_ctrl) @_listeners.dragged.unshift( _viewId ) unless ~@_listeners.dragged.indexOf( _viewId ) _ctrl.startDrag( @status.crsrX, @status.crsrY, @status.button2 ) # # Cancels text selections, which happen by _cancelTextSelection: -> # Remove possible selections. ELEM.get(0).focus() # # Patches IE classes that match css :active with .ieActive classes _ieClassNamePatched: [] _ieClassNamePatch: (_viewId)-> _elemId = @_views[_viewId].elemId _elem = ELEM.get( _elemId ) _ieClassNamePatched = @_ieClassNamePatched _ieClassNames = HThemeManager._ieActiveCssClassMatch _hasClassName = (_elem,_className)-> ~_elem.className.split(' ').indexOf(_className) _addClassName = (_elem)-> _classNames = _elem.className.trim().split(' ') _classNames.push('ieActive') _elem.className = _classNames.join(' ') _ieClassNamePatched.push(_elem) _level = 0 _patcher = (_elem)-> return if _level > 10 for _child in _elem.childNodes for _className in _ieClassNames _addClassName(_elem) if _hasClassName(_elem,_className) and not _hasClassName(_elem,'ieActive') continue if _child.view_id? _level += 1 _patcher(_child) _level -= 1 _patcher(_elem) _ieClassNameUnPatch: -> _ieClassNamePatched = @_ieClassNamePatched @_ieClassNamePatched = [] _delClassName = (_elem)-> _classNames = _elem.className.split(' ') _classNames.splice( _classNames.indexOf( 'ieActive' ), 1 ) _elem.className = _classNames.join(' ') for _elem in _ieClassNamePatched _delClassName(_elem) # # Mouse button press manager. Triggered by the global mouseDown event. # Delegates the following event responder methods to active HControl instances: # - mouseDown # - startDrag mouseDown: (e)-> @_modifiers(e) _leftClick = Event.isLeftClick(e) if _leftClick @status.setButton1( true ) else @status.setButton2( true ) _focused = @_listeners.focused _active = @_listeners.active _dragged = @_listeners.dragged _mouseDownable = @_listeners.byEvent.mouseDown _draggable = @_listeners.byEvent.draggable _newActive = [] _mouseDownIds = [] _startDragIds = [] _stop = false for _viewId in _focused _newActive.push( _viewId ) unless ~_active.indexOf(_viewId) _eventOptions = @_listeners.byId[_viewId] if ~_mouseDownable.indexOf( _viewId ) _mouseDownIds.push( _viewId ) if ~_draggable.indexOf( _viewId ) and not ~_dragged.indexOf( _viewId ) _startDragIds.push( _viewId ) for _viewId in _newActive _ctrl = @_views[_viewId] @changeActiveControl( _ctrl ) [ x, y ] = @status.crsr @_handleMouseMove(x,y) for _viewId in _mouseDownIds _ctrl = @_views[_viewId] _stop = true if _ctrl.mouseDown( x, y, _leftClick ) for _viewId in _startDragIds unless ~_dragged.indexOf( _viewId ) _ctrl = @_views[_viewId] _dragged.unshift( _viewId ) _stop = true if _ctrl.startDrag( x, y, _leftClick ) @_cancelTextSelection() unless _startDragIds.length == 0 and _mouseDownIds.length == 0 if BROWSER_TYPE.ie for _viewId in _focused @_ieClassNamePatch(_viewId) Event.stop(e) if _stop # # Mouse button press manager. Triggered by the global mouseDown event. # Delegates the following event responder methods to active HControl instances: # - mouseUp # - endDrag # - endHover # - drop mouseUp: (e)-> @_modifiers(e) _leftClick = Event.isLeftClick(e) @status.setButton1( false ) @status.setButton2( false ) return false unless _leftClick _focused = @_listeners.focused _active = @_listeners.active _dragged = @_listeners.dragged _hovered = @_listeners.hovered _mouseUppable = @_listeners.byEvent.mouseUp _draggable = @_listeners.byEvent.draggable _newActive = [] _mouseUpIds = [] _endDragIds = [] _stop = false for _viewId in _focused _newActive.push( _viewId ) unless ~_active.indexOf(_viewId) _eventOptions = @_listeners.byId[_viewId] if ~_mouseUppable.indexOf( _viewId ) _mouseUpIds.push( _viewId ) for _viewId in _dragged _endDragIds.push( _viewId ) [ x, y ] = @status.crsr @_handleMouseMove(x,y) for _viewId in _mouseUpIds _ctrl = @_views[_viewId] _stop = true if _ctrl.mouseUp( x, y, _leftClick ) for _viewId in _endDragIds _dragIdx = _dragged.indexOf( _viewId ) if ~_dragIdx _ctrl = @_views[_viewId] _dragged.splice( _dragIdx, 1 ) for _dropViewId in _hovered _dropCtrl = @_views[_dropViewId] _dropCtrl.endHover(_ctrl) if _dropCtrl.endHover? _dropCtrl.drop(_ctrl) if _dropCtrl.drop? _stop = true if _ctrl.endDrag( x, y, _leftClick ) for _viewId in _newActive _ctrl = @_views[_viewId] @changeActiveControl( _ctrl ) @_listeners.hovered = [] @_listeners.dragged = [] @_cancelTextSelection() unless _endDragIds.length == 0 and _mouseUpIds.length == 0 @_ieClassNameUnPatch() if BROWSER_TYPE.ie and @_ieClassNamePatched.length Event.stop(e) if _stop # # Handles mouse button clicks # It's different from mouseUp/mouseDown, because it's a different event, # and is supported by touch screen devices click: (e)-> @_modifiers(e) _leftClick = Event.isLeftClick(e) if _leftClick @status.setButton1( false ) else # there is a separate event for context menu, and only # Firefox fires click separately. # the handler is contextMenu return true # # Focus check here # _focused = @_listeners.focused _active = @_listeners.active _clickable = @_listeners.byEvent.click _doubleClickable = @_listeners.byEvent.doubleClick _doubleClickWait = [] _newActive = [] _clickIds = [] _stop = false for _viewId in _focused _newActive.push( _viewId ) unless ~_active.indexOf(_viewId) if ~_clickable.indexOf(_viewId) and ~_doubleClickable.indexOf(_viewId) _doubleClickWait.push( _viewId ) else if ~_clickable.indexOf(_viewId) _clickIds.push( _viewId ) for _viewId in _newActive _ctrl = @_views[_viewId] @changeActiveControl( _ctrl ) [ x, y ] = @status.crsr @_handleMouseMove(x,y) for _viewId in _clickIds _ctrl = @_views[_viewId] unless _ctrl.click? and typeof _ctrl.click == 'function' @warn( 'no click:', _ctrl, _ctrl.click?, typeof _ctrl.click ) continue _stop = true if _ctrl.click( x, y, _leftClick ) if _doubleClickWait.length _this = @ @dblClickWait = setTimeout( (-> for _viewId in _doubleClickWait _ctrl = _this._views[_viewId] unless _ctrl.click? and typeof _ctrl.click == 'function' _this.warn( 'no click:', _ctrl, _ctrl.click?, typeof _ctrl.click ) continue _stop = true if _ctrl.click( x, y, _leftClick ) Event.stop(e) if _stop ), 50 ) @_ieClassNameUnPatch() if BROWSER_TYPE.ie and @_ieClassNamePatched.length Event.stop(e) if _stop # # Handles doubleClick events doubleClick: (e)-> if @dblClickWait clearTimeout( @dblClickWait ) delete this['dblClickWait'] @_modifiers(e) _leftClick = Event.isLeftClick(e) @status.setButton1( false ) @status.setButton2( false ) [ x, y ] = @status.crsr @_handleMouseMove(x,y) _focused = @_listeners.focused _doubleClicks = [] _doubleClickable = @_listeners.byEvent.doubleClick _stop = false for _viewId in _focused if ~_doubleClickable.indexOf(_viewId) _doubleClicks.push( _viewId ) for _viewId in _doubleClicks _ctrl = @_views[_viewId] if _ctrl.doubleClick? _stop = true if _ctrl.doubleClick(x,y,true) Event.stop(e) if _stop # # Handles mouseWheel events (any HID scroll event) mouseWheel: (e)-> e = window.event unless e @_modifiers(e) if e.wheelDelta _delta = 0-(e.wheelDelta/120) else if e.detail _delta = 0-(e.detail/3) if BROWSER_TYPE.opera or BROWSER_TYPE.safari or BROWSER_TYPE.ie _delta = 0-_delta _focused = @_listeners.focused _mouseWheels = [] _mouseWheelable = @_listeners.byEvent.mouseWheel _stop = false for _viewId in _focused if ~_mouseWheelable.indexOf(_viewId) _mouseWheels.push( _viewId ) for _viewId in _mouseWheels _ctrl = @_views[_viewId] if _ctrl.mouseWheel? _stop = true if _ctrl.mouseWheel( _delta ) Event.stop(e) if _stop # # Handles the contextMenu event contextMenu: (e)-> @_modifiers(e) _stop = true @status.setButton1( false ) @status.setButton2( true ) _focused = @_listeners.focused _contextMenuable = @_listeners.byEvent.contextMenu for _viewId in _focused if ~_contextMenuable.indexOf(_viewId) _ctrl = @_views[_viewId] _stop = false if _ctrl.contextMenu? and _ctrl.contextMenu() Event.stop(e) if _stop # # Keycode translation tables _keyTrans: opera: # Symbol keys: 59: 186 # [;:] 61: 187 # [=+] 44: 188 # [,<] 45: 189 # [-_] 46: 190 # [.>] 47: 191 # [/?] 96: 192 # [`~] 91: 219 # [[{] 92: 220 # [\|] 93: 221 # []}] 39: 222 # ['"] # Numeric keypad keys can't be mapped on Opera, because Opera # doesn't differentiate between the keys on the numeric keypad # versus the functionally same keys elsewhere on the keyboard. # Branded keys: # Apple Command keys are same as ctrl, but ctrl is 0; Can't be re-mapped reliably. # The Windows Menu key also return 0, so it can't be re-mapped either. 219: 91 # Left Windows key (Start) 220: 92 # Right Windows key (Start) mozilla: # Symbol keys: 59: 186 # [;:] 61: 187 # [=+] 109: 189 # [-_] # Branded keys: 224: 91 # Apple Command key to left windows key mapping # # Translates keyCodes to the normalized pseudo-ascii used by IE and WebKit browsers. # Opera and Mozilla browsers use different codes, so they'll need translations. translateKeyCodes: (_keyCode)-> # We use the WebKit and IE browsers as the normalization base, because # there is no variance between in these. Returns the keyCode as-is for # browsers in this category. if BROWSER_TYPE.safari or BROWSER_TYPE.ie return _keyCode # Opera has its own keyCodes, which are different from all others. else if BROWSER_TYPE.opera _transCode = @_keyTrans.opera[_keyCode] # The assumption is that the other browsers do what mozille does. else _transCode = @_keyTrans.mozilla[_keyCode] # No translation needed: return _keyCode if not _transCode? or _transCode == null # Return translated: return _transCode # # List of keycodes considered command keys _cmdKeys: [ 17, # Ctrl 91, # Others (Left Start Key or Left Command Key) 92, # Others (Right Start Key) 93 # Others (Menu Key or Right Command Key) ] _detectCmdKey: ( _keyCode )-> # On Opera, return true on any of the keycodes if BROWSER_TYPE.opera return !!~@_cmdKeys.indexOf(_keyCode) # Any mac browser (except opera, above) uses left or right windows key # equivalent as the Command key. else if BROWSER_TYPE.mac return _keyCode == 91 or _keyCode == 93 # Other platforms use CTRL as the command key. return _keyCode == 17 # # Special key method handlers for ESC and RETURN, maybe others in the future _defaultKeyActions: '13': 'defaultKey' # return '27': 'escKey' # esc # # Traverses down the parent hierarchy searching for a parent object # that responds true to _methodName. If _ctrl is undefined, use # a special default rule of auto-selecting the active control and # checking all of its siblings before traversing. defaultKey: (_methodName, _ctrl, _testedIds) -> return true if _ctrl?[_methodName]?() == true return null unless @_listeners.active _ctrl = @_views[@_listeners.active[0]] unless _ctrl? return null unless _ctrl? _ctrlId = _ctrl.viewId return null if ~_testedIds.indexOf(_ctrlId) return true if _ctrl?[_methodName]?() == true _stop = null _testedIds.push(_ctrlId) for _viewId in _ctrl.parent.views continue if ~_testedIds.indexOf(_viewId) continue if _ctrl? and _ctrl.viewId == _viewId _ctrl = @_views[_viewId] if _ctrl?[_methodName]? _stopStatus = _ctrl[_methodName]() if _stopStatus == false or _stopStatus == true _stop = _stopStatus unless _stop return _stop if _stop != null return true if _ctrl?.parent? and @defaultKey(_methodName, _ctrl.parent, _testedIds) == true null # # Handles the keyDown event keyDown: (e)-> @_modifiers(e) _keyCode = @translateKeyCodes( e.keyCode ) _stop = false if !@status.cmdKeyDown and @_detectCmdKey( _keyCode ) @status.setCmdKey( true ) _stop = true _active = @_listeners.active _keyDowners = @_listeners.byEvent.keyDown _keyRepeaters = @_listeners.byEvent.keyRepeat _keyDowns = [] _repeating = ( @_lastKeyDown == _keyCode ) and @status.hasKeyDown( _keyCode ) # unless @status.hasKeyDown( _keyCode ) for _viewId in _keyDowners if !_repeating or ~_keyRepeaters.indexOf( _viewId ) _keyDowns.push( _viewId ) if ~_active.indexOf( _viewId ) @status.addKeyDown( _keyCode ) for _viewId in _keyDowns _ctrl = @_views[_viewId] if _ctrl.keyDown? _stop = true if _ctrl.keyDown(_keyCode) # # Some keys are special (esc and return) and they have their own # special events: defaultKey and escKey, which aren't limited # to instances of HControl, but any parent object will work. if not _repeating and @_defaultKeyActions[_keyCode.toString()] _defaultKeyMethod = @_defaultKeyActions[_keyCode.toString()] _stop = true if @defaultKey(_defaultKeyMethod,null,[]) @_lastKeyDown = _keyCode Event.stop(e) if _stop keyUp: (e)-> @_modifiers(e) _keyCode = @translateKeyCodes( e.keyCode ) _stop = false if @status.cmdKeyDown and @_detectCmdKey( _keyCode ) @status.setCmdKey( false ) _stop = true _active = @_listeners.active _enabled = @_listeners.enabled _keyUppers = @_listeners.byEvent.keyUp _keyUps = [] _textEnterers = @_listeners.byEvent.textEnter _textEnters = [] for _viewId in _textEnterers _textEnters.push( _viewId ) if ~_enabled.indexOf( _viewId ) for _viewId in _textEnters _ctrl = @_views[_viewId] if _ctrl.textEnter? _stop = true if _ctrl.textEnter( _keyCode ) if @status.hasKeyDown( _keyCode ) for _viewId in _active _keyUps.push( _viewId ) if ~_keyUppers.indexOf( _viewId ) @status.delKeyDown( _keyCode ) for _viewId in _keyUppers _ctrl = @_views[_viewId] if _ctrl.keyUp? and ~_active.indexOf( _viewId ) _stop = true if _ctrl.keyUp( _keyCode ) Event.stop(e) if _stop keyPress: (e)-> # @warn('EventManager#keyPress not implemented') isKeyDown: (_keyCode)-> @warn('EventManager#isKeyDown() is deprecated, use #status.hasKeyDown() instead') @status.hasKeyDown( _keyCode ) isKeyUp: (_keyCode)-> @warn('EventManager#isKeyUp() is deprecated, use !#status.hasKeyDown() instead') (not @status.hasKeyDown( _keyCode )) isAltKeyDown: -> @warn('EventManager#isAltKeyDown is deprecated, use #status.altKeyDown instead') @status.altKeyDown isCtrlKeyDown: -> @warn('EventManager#isCtrlKeyDown is deprecated, use #status.ctrlKeyDown instead') @status.ctrlKeyDown isShiftKeyDown: -> @warn('EventManager#isShiftKeyDown is deprecated, use #status.shiftKeyDown instead') @status.shiftKeyDown isMetaKeyDown: -> @warn('EventManager#isMetaKeyDown is deprecated, use #status.metaKeyDown instead') @status.metaKeyDown isCmdKeyDown: -> @warn('EventManager#isCmdKeyDown is deprecated, use #status.cmdKeyDown instead') @status.altKeyDown # idle: -> if @_topmostQueue.length _items = [] _lastPoint = false for i in [0..(@_topmostQueue.length-1)] [ _point, _ctrl ] = @_topmostQueue.shift() _lastPoint = _point continue if _ctrl.isDead continue unless _ctrl.enabled _items.push( _ctrl ) _matchIds = @_findTopmostEnabled( _lastPoint, 'contains', null ) for _ctrl in _items _viewId = _ctrl.viewId if ~_matchIds.indexOf( _viewId ) @changeActiveControl( _ctrl ) # Debug output # console.log( 'focused: ', # JSON.stringify(@_listeners.focused),'active:', # JSON.stringify(@_listeners.active),'dragged:', # JSON.stringify(@_listeners.dragged),'hovered:', # JSON.stringify(@_listeners.hovered) ) # # Cleans up structures die: -> @stop() @base() LOAD( -> window.EventManager = EventManagerApp.new( 40, 'EventManager' ) # Alias: window.EVENT = EventManager EventManager.start() )