###* Events

Up.js has a convenient way to [listen to DOM events](/up.on):

up.on('click', 'button', function(event, $button) {
  // $button is a jQuery collection containing
  // the clicked <button> element
});

This is roughly equivalent to binding an event listener to `document` using jQuery's [`on`](api.jquery.com/on/).

Many Up.js interactions also emit DOM events that are prefixed with `up:`.

up.on('up:modal:opened', function(event) {
  console.log('A new modal has just opened!');
});

Events often have both present (`up:modal:open`) and past forms (`up:modal:opened`). You can usually prevent an action by listening to the present form and call `preventDefault()` on the `event` object:

up.on('up:modal:open', function(event) {
  if (event.url == '/evil') {
    // Prevent the modal from opening
    event.preventDefault();
  }
});

@class up.bus ### up.bus = (($) ->

u = up.util

# We remember which argument lists have been passed to `up.on`
# so we can clean out the listener registry between tests.
liveUpDescriptions = {}
nextUpDescriptionNumber = 0

###*
Convert an Up.js style listener (second argument is the event target
as a jQuery collection) to a vanilla jQuery listener

@function upListenerToJqueryListener
@internal
###
upListenerToJqueryListener = (upListener) ->
  (event) ->
    $me = event.$element || $(this)
    upListener.apply($me.get(0), [event, $me, up.syntax.data($me)])

###*
Converts an argument list for `up.on` to an argument list for `jQuery.on`.
This involves rewriting the listener signature in the last argument slot.

@function upDescriptionToJqueryDescription
@internal
###
upDescriptionToJqueryDescription = (upDescription, isNew) ->
  jqueryDescription = u.copy(upDescription)
  upListener = jqueryDescription.pop()
  jqueryListener = undefined
  if isNew
    jqueryListener = upListenerToJqueryListener(upListener)
    upListener._asJqueryListener = jqueryListener
    upListener._descriptionNumber = ++nextUpDescriptionNumber
  else
    jqueryListener = upListener._asJqueryListener
    jqueryListener or u.error('up.off: The event listener %o was never registered through up.on')
  jqueryDescription.push(jqueryListener)
  jqueryDescription

###*
Listens to an event on `document`.

The given event listener which will be executed whenever the
given event is [triggered](/up.emit) on the given selector:

    up.on('click', '.button', function(event, $element) {
      console.log("Someone clicked the button %o", $element);
    });

This is roughly equivalent to binding an event listener to `document`:

    $(document).on('click', '.button', function(event) {
      console.log("Someone clicked the button %o", $(this));
    });

Other than jQuery, Up.js will silently discard event listeners
on [unsupported browsers](/up.browser.isSupported).

\#\#\#\# Attaching structured data

In case you want to attach structured data to the event you're observing,
you can serialize the data to JSON and put it into an `[up-data]` attribute:

    <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span>
    <span class="person" up-data="{ age: 22, name: 'Jim' }">Jim</span>

The JSON will parsed and handed to your event handler as a third argument:

    up.on('click', '.person', function(event, $element, data) {
      console.log("This is %o who is %o years old", data.name, data.age);
    });

\#\#\#\# Migrating jQuery event handlers to `up.on`

Within the event handler, Up.js will bind `this` to the
native DOM element to help you migrate your existing jQuery code to
this new syntax.

So if you had this before:

    $(document).on('click', '.button', function() {
      $(this).something();
    });

... you can simply copy the event handler to `up.on`:

    up.on('click', '.button', function() {
      $(this).something();
    });

\#\#\#\# Stopping to listen

`up.on` returns a function that unbinds the event listeners when called.

There is also a function [`up.off`](/up.off) which you can use for the same purpose.

@function up.on
@param {String} events
  A space-separated list of event names to bind.
@param {String} [selector]
  The selector of an element on which the event must be triggered.
  Omit the selector to listen to all events with that name, regardless
  of the event target.
@param {Function(event, $element, data)} behavior
  The handler that should be called.
  The function takes the affected element as the first argument (as a jQuery object).
  If the element has an `up-data` attribute, its value is parsed as JSON
  and passed as a second argument.
@return {Function}
  A function that unbinds the event listeners when called.
@stable
###
live = (upDescription...) ->
  # Silently discard any event handlers that are registered on unsupported
  # browsers and return a no-op destructor
  return (->) unless up.browser.isSupported()

  # Convert the args for up.on to an argument list as expected by jQuery.on.
  jqueryDescription = upDescriptionToJqueryDescription(upDescription, true)

  # Remember the descriptions we registered, so we can
  # clean up after ourselves during a `reset`
  rememberUpDescription(upDescription)

  $(document).on(jqueryDescription...)

  # Return destructor
  -> unbind(upDescription...)

###*
Unregisters an event listener previously bound with [`up.on`](/up.on).

\#\#\#\# Example

Let's say you are listing to clicks on `.button` elements:

    var listener = function() { };
    up.on('click', '.button', listener);

You can stop listening to these events like this:

    up.off('click', '.button', listener);

Note that you need to pass `up.off` a reference to the same listener function
that was passed to `up.on` earlier.

@function up.off
@stable
###
unbind = (upDescription...) ->
  jqueryDescription = upDescriptionToJqueryDescription(upDescription, false)
  forgetUpDescription(upDescription)
  $(document).off(jqueryDescription...)

rememberUpDescription = (upDescription) ->
  number = upDescriptionNumber(upDescription)
  liveUpDescriptions[number] = upDescription

forgetUpDescription = (upDescription) ->
  number = upDescriptionNumber(upDescription)
  delete liveUpDescriptions[number]

upDescriptionNumber = (upDescription) ->
  u.last(upDescription)._descriptionNumber

###*
Emits a event with the given name and properties.

The event will be triggered as a jQuery event on `document`.

Other code can subscribe to events with that name using
[`up.on`](/up.on) or by [binding a jQuery event listener](http://api.jquery.com/on/) to `document`.

\#\#\#\# Example

    up.on('my:event', function(event) {
      console.log(event.foo);
    });

    up.emit('my:event', { foo: 'bar' });
    # Prints "bar" to the console

@function up.emit
@param {String} eventName
  The name of the event.
@param {Object} [eventProps={}]
  A list of properties to become part of the event object
  that will be passed to listeners. Note that the event object
  will by default include properties like `preventDefault()`
  or `stopPropagation()`.
@param {jQuery} [eventProps.$element=$(document)]
  The element on which the event is triggered.
@param {String|Array} [eventProps.message]
  A message to print to the console when the event is emitted.
  If omitted, a default message is printed.
  Set this to `false` to prevent any console output.
@experimental
###
emit = (eventName, eventProps = {}) ->
  event = $.Event(eventName, eventProps)
  if $target = eventProps.$element
    delete eventProps.$element
  else
    $target = $(document)
  logEmission(eventName, eventProps)
  $target.trigger(event)
  event

logEmission = (eventName, eventProps) ->
  if eventProps.hasOwnProperty('message')
    niceMessage = eventProps.message
    delete eventProps.message
    if u.isArray(niceMessage)
      [niceMessage, niceMessageArgs...] = niceMessage
    else
      niceMessageArgs = []
    if niceMessage
      if u.isPresent(eventProps)
        up.puts "#{niceMessage} (%s (%o))", niceMessageArgs..., eventName, eventProps
      else
        up.puts "#{niceMessage} (%s)", niceMessageArgs..., eventName
  else
    if u.isPresent(eventProps)
      up.puts 'Emitted event %s (%o)', eventName, eventProps
    else
      up.puts 'Emitted event %s', eventName

###*
[Emits an event](/up.emit) and returns whether any listener
has prevented the default action.

@function up.bus.nobodyPrevents
@param {String} eventName
@param {Object} eventProps
@param {String|Array} [eventProps.message]
@experimental
###
nobodyPrevents = (args...) ->
  event = emit(args...)
  if event.isDefaultPrevented()
    up.puts "An observer prevented the event %s", args[0]
    false
  else
    true

###*
Registers an event listener to be called when the user
presses the `Escape` key.

@function up.bus.onEscape
@param {Function} listener
  The listener function to register.
@return {Function}
  A function that unbinds the event listeners when called.
@experimental
###
onEscape = (listener) ->
  live('keydown', 'body', (event) ->
    if u.escapePressed(event)
      listener(event)
  )

###*
Makes a snapshot of the currently registered event listeners,
to later be restored through [`up.bus.reset`](/up.bus.reset).

@internal
###
snapshot = ->
  for description in liveUpDescriptions
    description.isDefault = true

###*
Resets the list of registered event listeners to the
moment when the framework was booted.

@internal
###
restoreSnapshot = ->
  doomedDescriptions = u.reject(liveUpDescriptions, (description) -> description.isDefault)
  unbind(description...) for description in doomedDescriptions

###*
Resets Up.js to the state when it was booted.
All custom event handlers, animations, etc. that have been registered
will be discarded.

This is an internal method for to enable unit testing.
Don't use this in production.

@function up.reset
@experimental
###
emitReset = ->
  up.emit('up:framework:reset', message: 'Resetting framework')

###*
This event is [emitted](/up.emit) when Up.js is [reset](/up.reset) during unit tests.

@event up:framework:reset
@experimental
###

###*
Boots the Up.js framework.

This is done automatically by including the Up.js Javascript.

Up.js will not boot if the current browser is [not supported](/up.browser.isSupported).
This leaves you with a classic server-side application on legacy browsers.

Emits the [`up:framework:boot`](/up:framework:boot) event.

@function up.boot
@experimental
###
boot = ->
  if up.browser.isSupported()
    # Can't decouple this via the event bus, since up.bus would require
    # up.browser.isSupported() and up.browser would require up.on()
    up.browser.installPolyfills()
    up.emit('up:framework:boot', message: 'Booting framework')

###*
This event is [emitted](/up.emit) when Up.js [boots](/up.boot).

@event up:framework:boot
@experimental
###

live 'up:framework:boot', snapshot
live 'up:framework:reset', restoreSnapshot

knife: eval(Knife?.point)
on: live # can't name symbols `on` in Coffeescript
off: unbind # can't name symbols `off` in Coffeescript
emit: emit
nobodyPrevents: nobodyPrevents
onEscape: onEscape
emitReset: emitReset
boot: boot

)(jQuery)

up.on = up.bus.on up.off = up.bus.off up.emit = up.bus.emit up.reset = up.bus.emitReset up.boot = up.bus.boot