coffeescript-ui
Version:
Coffeescript User Interface System
440 lines (367 loc) • 11.4 kB
text/coffeescript
###
* coffeescript-ui - Coffeescript User Interface System (CUI)
* Copyright (c) 2013 - 2016 Programmfabrik GmbH
* MIT Licence
* https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###
# Events Inter-"Process"-Communication for CUI
#
#
#
# @example How to listen to and trigger events
#
# Event.listen
# type: [ "click", "dblclick" ]
# node: jQuery Element or CUI DOM Element
# call: (ev, info) ->
# selector: jQuery like path selector to filter events
#
#
# Event.trigger
# type: "content-resize"
# node: jQuery Element or CUI DOM Element
# bubble: set to yes if event should bubble up or down the DOM tree
# info: info Map, contains eventsEvent for DOMElements and
# the internal "waits" queue
#
# Event.ignore
# type: "<type>"
# node: jQuery or DOM Element
#
#
# CUIEvents bound to a node will be checked for the existance in the
# DOM tree prio execution. If they don't exist (after they
# have been inserted), the CUI.Listener will be deleted.
#
# All events need to be registered or a warning is output at the console.
#
# CUI.Events.registerEvent(options)
# options are the default options for the event
# type: <type>
# bubble: true|false
#
#
class CUI.Events extends CUI.Element
@defaults:
maxWait: 1500
@__listeners = []
@__eventRegistry = {}
@__getListenersForNode: (node) ->
if node == document or node == window
@__listeners
else
CUI.dom.data(node, "listeners")
@__registerListener: (listener) ->
CUI.util.assert(listener instanceof CUI.Listener, "CUI.Events.__registerListener", "listener needs to be instance of Listener", listener: listener)
node = listener.getNode()
listeners = @__getListenersForNode(node)
if not listeners
listeners = []
CUI.dom.data(node, "listeners", listeners)
listeners.push(listener)
if node instanceof HTMLElement
node.setAttribute("data-cui-listeners", "")
@
@__getActiveListeners: (doc=document) ->
if doc == document
listeners = @__listeners.slice(0)
else
listeners = []
if CUI.dom.matches(doc, '[data-cui-listeners]')
listeners.push.apply(listeners, CUI.dom.data(doc, "listeners"))
for el in CUI.dom.matchSelector(doc, "[data-cui-listeners]")
listeners.push.apply(listeners, CUI.dom.data(el, "listeners"))
listeners
@unregisterListener: (listener) ->
node = listener.getNode()
arr = @__getListenersForNode(node)
CUI.util.assert(arr, "CUI.Events.unregisterListeners", "Listeners not found for node.", node: node, listener: listener)
# console.error "unregistring listeenr", listener.getUniqueId()
CUI.util.removeFromArray(listener, arr)
if arr.length == 0 and node instanceof HTMLElement
node.removeAttribute("cui-events-listener-element")
CUI.dom.removeData(node, "listeners")
# console.debug "removing listeners from node", node[0]
@
# wait for an event on a node
@wait: (_opts) ->
opts = CUI.Element.readOpts _opts, "CUI.Events.wait",
# event type
type:
mandatory: true
check: String
node:
mandatory: true
check: (v) ->
CUI.dom.isNode(v)
# optionally wait for a timeout
# if set to <= 0, wait forever
maxWait:
default: CUI.defaults.class.Events.defaults.maxWait
check: (v) ->
i = parseInt(v)
if isNaN(i)
false
else if i == -1
true
else if i >= 0
true
else
false
dfrs = []
listeners = []
_node = CUI.dom.getNode(opts.node)
dfr = new CUI.Deferred()
listeners.push CUI.Events.listen
type: opts.type
node: _node
call: ->
dfr.resolve()
return
dfrs.push(dfr)
master_dfr = new CUI.Deferred()
master_dfr.always ->
for listener in listeners
listener.destroy()
return
CUI.when(dfrs)
.fail ->
master_dfr.reject()
.done ->
master_dfr.resolve()
if opts.maxWait >= 0
CUI.setTimeout
ms: opts.maxWait
call: ->
for dfr in dfrs
if dfr.state() == "pending"
dfr.reject()
return
master_dfr.promise()
# register a listener
# @param listener PlainObject or CUI.Listener
@listen: (_listener) ->
listener = CUI.Listener.require(_listener, "CUI.Events.listen")
@__registerListener(listener)
listener
@trigger: (_event) ->
event = CUI.Event.require(_event, "CUI.Events.trigger")
# allwo event calls to return a promise
# wait for all promises before
# returning from this methos
#
info = event.getInfo()
waits = []
info.__waits = waits
# use the standard event system for this kind of events
bubble = event.isBubble()
sink = event.isSink()
exclude = event.isExcludeSelf()
node = event.getNode()
# console.debug "trigger event", event.getType(), info, event.getUniqueId(), bubble, sink, exclude, event.isInDOM()
if bubble or not event.isInDOM()
event.dispatch()
else
# dispatch sets a native event and through that, the event
# gains a target. without the dispatch, we need to set
# the target for this event
event.setTarget(node)
if exclude and not bubble and not sink
CUI.util.assert(false, "CUI.Events.trigger", "Unable to trigger event with bubble == false, sink == false and exclude_self == true.", event: event)
if sink or (not sink and not bubble and not exclude and event.isInDOM())
# if event.getType() == "toolbox"
# console.debug "sink event...", event.getType(), event.getNode()
triggerListeners = []
for listener, idx in @__getActiveListeners()
if event.getType() not in listener.getTypes()
continue
if listener.matchesEvent(event) == null
continue
# console.error "triggering...", event.getType()
triggerListeners.push(listener)
# if triggerListeners.length == 0
# console.warn("CUI.Events.trigger: No listeners found for Event #{event.getType()}.", event: event, activeListeners: @active())
triggerListeners.sort (a, b) ->
CUI.util.compareIndex(a.getDepthFromLastMatchedEvent(), b.getDepthFromLastMatchedEvent())
stopNodes = []
ev_node = event.getNode()
for listener in triggerListeners
listener_node = listener.getNode()
# console.debug "listener:", listener, listener.getDepthFromLastMatchedEvent()
if listener_node and stopNodes.length > 0
listener_node_parents = CUI.dom.parents(listener_node)
skip = false
for stopNode in stopNodes
for listener_node_parent in listener_node_parents
if listener_node_parent == stopNode
skip = true
break
if skip
# node is below the stop node, skip
continue
event.setCurrentTarget(listener_node)
listener.handleEvent(event, "sink")
if event.isImmediatePropagationStopped()
# console.debug "immediate stopped!"
break
# add to stop nodes if the depth is at least 0 meaning that
# the listener has a node
if event.isPropagationStopped() and listener_node
# console.debug "adding stop node", listener_node[0]
stopNodes.push(listener_node)
return CUI.when(waits)
@ignore: (filter, doc=document) -> # , debug=false) ->
# console.debug "CUI.Events.ignore", filter, filter.instance?.getUniqueId?()
for listener in @__getActiveListeners(doc)
if not filter or CUI.util.isEmptyObject(filter) or listener.matchesFilter(filter)
# if debug
# console.debug("CUI.Events.ignore: ignoring listener:", listener.getNode(), DOM.data(listener.getNode()).listeners?.length, filter.instance?.getUniqueId?())
listener.destroy()
@
@dump: (filter={}) ->
for listener in @__getActiveListeners()
if CUI.util.isEmptyObject(filter) or listener.matchesFilter(filter)
console.debug("Listener", listener.getTypes(), (if listener.getNode() then "NODE" else "-"), listener)
@
@dumpTopLevel: ->
for listener in @__listeners
console.debug("Listener [document, window]", listener.getTypes(), listener.getInstance())
for listener in CUI.dom.data(document.documentElement, "listeners")
console.debug("Listener [document.documentElement]", listener.getTypes(), listener.getInstance(), listener)
@
@hasEventType: (type) ->
!!@__eventRegistry[type]
# returns event info by type
@getEventType: (type) ->
ev = @__eventRegistry[type]
CUI.util.assert(ev, "Unknown event type \"#{type}\". Use CUI.Events.registerEvent to register this type.")
return ev
@getEventTypeAliases: (type) ->
@getEventType(type).alias or [type]
@registerEvent: (event, allow_array=true) ->
if not CUI.util.isArray(event.type) or not allow_array
CUI.util.assert(CUI.util.isString(event?.type) and event.type.length > 0, "CUI.Events.registerEvent", "event.type must be String.", event: event)
register_other_type = (_type) =>
_event = CUI.util.copyObject(event, true)
_event.type = _type
@registerEvent(_event, false)
if CUI.util.isArray(event.type)
for type in event.type
register_other_type(type)
else
if event.hasOwnProperty("DOMEvent")
console.error("event.DOMEvent is obsolete")
delete(event.DOMEvent)
if event.hasOwnProperty("CUIEvent")
console.error("event.CUIEvent is obsolete")
delete(event.CUIEvent)
@__eventRegistry[event.type] = event
if event.alias
for type in event.alias
if not @__eventRegistry[type]
register_other_type(type)
@
@__init: ->
defaults =
BrowserEvents:
bubble: true
DOM:
bubble: true
CUI:
eventClass: CUI.CUIEvent
sink: true
KeyboardEvents:
eventClass: CUI.KeyboardEvent
bubble: true
MouseEvents:
eventClass: CUI.MouseEvent
bubble: true
TouchEvents:
eventClass: CUI.TouchEvent
bubble: true
for block, events of {
MouseEvents:
mousemove: {}
mouseover: {}
mouseout: {}
mouseleave: {}
mouseenter: {}
wheel:
eventClass: CUI.WheelEvent
bubble: false
mousedown: {}
mouseup: {}
click: {}
dblclick: {}
contextmenu: {}
TouchEvents:
touchstart: {}
touchend: {}
touchmove: {}
touchcancel: {}
touchforchange: {}
gesturestart: {}
gestureend: {}
gesturechange: {}
KeyboardEvents:
input:
bubble: false
keyup: {}
keydown: {}
keypress: {}
BrowserEvents:
beforeunload: {}
unload: {}
load: {}
error: {}
close: {}
popstate: {}
dragstart: {}
dragleave: {}
dragenter: {}
message: {}
fullscreenchange:
alias: "fullscreenchange mozfullscreenchange webkitfullscreenchange MSFullscreenChange".split(" ")
hashchange:
bubble: false
change:
bubble: false
focus:
bubble: false
blur:
bubble: false
paste:
bubble: false
dragover:
bubble: false
drop:
bubble: false
scroll:
bubble: false
selectstart:
bubble: false
animationstart:
alias: "animationstart MSAnimationStart webkitAnimationStart".split(" ")
bubble: false
animationend:
alias: "animationend MSAnimationEnd webkitAnimationEnd".split(" ")
bubble: false
transitionend:
alias: "transitionend webkitTransitionEnd MSTransitionEnd".split(" ")
bubble: false
resize:
bubble: false
DOM:
"content-resize":
eventClass: CUI.CUIEvent
CUI:
# "load_server": {}
# "unload_server": {}
"viewport-resize": {}
}
for type, ev of events
CUI.util.mergeMap(ev, defaults[block])
ev.type = type
@registerEvent(ev)
CUI.Events.__init()
CUI.defaults.class.Events = CUI.Events