# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# ## t5/core/dom
#
# This is the abstraction layer that allows the majority of components to operate without caring whether the
# underlying infrastructure framework is Prototype, jQuery, or something else.
#
# The abstraction layer has a number of disadvantages:
#
# * It adds a number of layers of wrapper around the infrastructure framework objects
# * It is leaky; some behaviors will vary slightly based on the active infrastructure framework
# * The abstraction is alien to both Prototype and jQuery developers; it mixes some ideas from both
# * It is much less powerful or expressive than either infrastructure framework used directly
#
# It is quite concievable that some components will require direct access to the infrastructure framework, especially
# those that are wrappers around third party libraries or plugins; however many simple components may need no more than
# the abstract layer and gain the valuable benefit of not caring about the infrastructure framework.

#if prototype
define ["underscore", "./utils", "./events", "jquery"],
(_, utils, events) ->
#elseif jquery
define ["underscore", "./utils", "./events", "jquery"],
(_, utils, events, $) ->
#endif

#if prototype
  # Save a local reference to Prototype.$ ... see notes about some challenges using Prototype, jQuery,
  # and RequireJS together, here: https://github.com/jrburke/requirejs/issues/534
  $ = window.$

  # Fires a native event; something that Prototype does not normally do.
  # Returns true if the event completed normally, false if it was canceled.
  fireNativeEvent = (element, eventName) ->
    if document.createEventObject
      # IE support:
      event = document.createEventObject()
      return element.fireEvent "on#{eventName}", event

    # Everyone else:
    event = document.createEvent "HTMLEvents"
    event.initEvent eventName, true, true
    element.dispatchEvent event
    return not event.defaultPrevented

  # converts a selector to an array of DOM elements
  parseSelectorToElements = (selector) ->
    if _.isString selector
      return $$ selector

    # Array is assumed to be array of DOM elements
    if _.isArray selector
      return selector

    # Assume its a single DOM element

    [selector]
#endif

  # Converts content (provided to `ElementWrapper.update()` or `append()`) into an appropriate type. This
  # primarily exists to validate the value, and to "unpack" an ElementWrapper into a DOM element.
  convertContent = (content) ->
    if _.isString content
      return content

    if _.isElement content
      return content

    if content instanceof ElementWrapper
#if jquery
      return content.$
#elseif prototype
      return content.element
#endif

    throw new Error "Provided value <#{content}> is not valid as DOM element content."

  # Generic view of an DOM event that is passed to a handler function.
  #
  # Properties:
  #
  # * nativeEvent - the native Event object, which may provide additional information.
  # * memo - the object passed to `ElementWrapper.trigger()`.
  # * type - the name of the event that was triggered.
  # * char - the character value of the pressed key, if a printable character, as a string.
  # * key -The key value of the pressed key. This is the same as the `char` property for printable keys,
  #  or a key name for others.
  class EventWrapper

#if jquery
    constructor: (event, memo) ->
      @nativeEvent = event
      @memo = memo
#elseif prototype
    constructor: (event) ->
      @nativeEvent = event
      @memo = event.memo
#endif
      # This is to satisfy YUICompressor which doesn't seem to like 'char', even
      # though it doesn't appear to be a reserved word.
      this[name] = event[name] for name in ["type", "char", "key"]

    # Stops the event which prevents further propagation of the DOM event,
    # as well as DOM event bubbling.
    stop: ->
#if jquery
      @nativeEvent.preventDefault()
      @nativeEvent.stopImmediatePropagation()
#elseif prototype
      # There's no equivalent to stopImmediatePropagation() unfortunately.
      @nativeEvent.stop()
#endif

#if jquery
  # Interface between the dom's event model, and jQuery's.
  #
  # * jqueryObject - jQuery wrapper around one or more DOM elements
  # * eventNames - space-separated list of event names
  # * match - selector to match bubbled elements, or null
  # * handler - event handler function to invoke; it will be passed an `EventWrapper` instance as the first parameter,
  #   and the memo as the second parameter. `this` will be the `ElementWrapper` for the matched element.
  #
  # Event handlers may return false to stop event propogation; this prevents an event from bubbling up, and
  # prevents any browser default behavior from triggering.  This is often easier than accepting the `EventWrapper`
  # object as the first parameter and invoking `stop()`.
  #
  # Returns a function of no parameters that removes any added handlers.

  onevent = (jqueryObject, eventNames, match, handler) ->
    throw new Error "No event handler was provided." unless handler?

    wrapped = (jqueryEvent, memo) ->
      # Set `this` to be the matched ElementWrapper, rather than the element on which the event is observed
      # (which is often further up the hierarchy).
      elementWrapper = new ElementWrapper $(jqueryEvent.target)
      eventWrapper = new EventWrapper jqueryEvent, memo

      result = handler.call elementWrapper, eventWrapper, memo

      # If an event handler returns exactly false, then stop the event.
      if result is false
        eventWrapper.stop()

      return

    jqueryObject.on eventNames, match, wrapped

    # Return a function to stop listening
    -> jqueryObject.off eventNames, match, wrapped
#elseif prototype
  # Interface between the dom's event model, and Prototype's.
  #
  # * elements - array of DOM elements (or the document object)
  # * eventNames - array of event names
  # * match - selector to match bubbled elements, or null
  # * handler - event handler function to invoke; it will be passed an `EventWrapper` instance as the first parameter,
  #   and the memo as the second parameter. `this` will be the `ElementWrapper` for the matched element.
  #
  # Event handlers may return false to stop event propagation; this prevents an event from bubbling up, and
  # prevents any browser default behavior from triggering.  This is often easier than accepting the `EventWrapper`
  # object as the first parameter and invoking `stop()`.
  #
  # Returns a function of no parameters that removes any added handlers.

  onevent = (elements, eventNames, match, handler) ->
      throw new Error "No event handler was provided." unless handler?

      wrapped = (prototypeEvent) ->
        # Set `this` to be the matched ElementWrapper, rather than the element on which the event is observed
        # (which is often further up the hierarchy).
        elementWrapper = new ElementWrapper prototypeEvent.findElement()
        eventWrapper = new EventWrapper prototypeEvent

        # Because there's no stopImmediatePropogation() as with jQuery, we detect if the
        # event was stopped and simply stop calling the handler.
        result = if prototypeEvent.stopped
                  false
                else handler.call elementWrapper, eventWrapper, eventWrapper.memo

        # If an event handler returns exactly false, then stop the event.
        if result is false
          prototypeEvent.stop()

        return

      eventHandlers = []

      for element in elements
        for eventName in eventNames
          eventHandlers.push (Event.on element, eventName, match, wrapped)

      # Return a function to remove the handler(s)
      ->
        for eventHandler in eventHandlers
          eventHandler.stop()
#endif

  # Wraps a DOM element, providing some common behaviors.
  # Exposes the DOM element as property `element`.
  class ElementWrapper

#if jquery
    # Passed the jQuery object
    constructor: (query) ->
      @$ = query
      @element = query[0]
#elseif prototype
    constructor: (@element) ->
#endif

    # Some coders would use some JavaScript cleverness to automate more of the mapping from the ElementWrapper API
    # to the jQuery API, but that eliminates a chance to write some very necessary documentation.

    toString: ->
      markup = @element.outerHTML

      "ElementWrapper[#{markup.substring 0, (markup.indexOf ">") + 1}]"

    # Hides the wrapped element, setting its display to 'none'.
    hide: ->
#if jquery
      @$.hide()
#elseif prototype
      @element.hide()
#endif

      return this

    # Displays the wrapped element if hidden.
    show: ->
#if jquery
      @$.show()
#elseif prototype
      @element.show()
#endif

      return this

    # Gets or sets a CSS property. jQuery provides a lot of mapping of names to canonical names.
    css: (name, value) ->

      if arguments.length is 1
#if jquery
        return @$.css name
#elseif prototype
        return @element.getStyle name
#endif

#if jquery
      @$.css name, value
#elseif prototype
      @element.setStyle name: value
#endif

      return this

    # Returns the offset of the object relative to the document. The returned object has
    # keys `top`' and `left`'.
    offset: ->
#if jquery
      @$.offset()
#elseif prototype
      @element.viewportOffset()
#endif

    # Removes the wrapped element from the DOM.  It can later be re-attached.
    remove: ->
#if jquery
      @$.detach()
#elseif prototype
      @element.remove()
#endif

      return this

    # Reads or updates an attribute. With one argument, returns the current value
    # of the attribute. With two arguments, updates the attribute's value, and returns
    # the previous value. Setting an attribute to null is the same as removing it.
    #
    # Alternately, the first attribute can be an object in which case all the keys
    # and values of the object are applied as attributes, and this `ElementWrapper` is returned.
    #
    # * name - the attribute to read or update, or an object of keys and values
    # * value - (optional) the new value for the attribute
    attr: (name, value) ->

      if _.isObject name
        for attributeName, value of name
          @attr attributeName, value

        return this

#if jquery
      current = @$.attr name
      if arguments.length > 1
        if value is null
          @$.removeAttr name
        else
          @$.attr name, value
      if _.isUndefined current
        current = null
#elseif prototype
      current = @element.readAttribute name
      if arguments.length > 1
        # Treat undefined and null the same; Prototype does something slightly odd,
        # treating undefined as a special case where the attribute value matches
        # the attribute name.
        @element.writeAttribute name, if value is undefined then null else value
#endif

      return current

    # Moves the cursor to the field.
    focus: ->
#if jquery
      @$.focus()
#elseif prototype
      @element.focus()
#endif

      return this

    # Returns true if the element has the indicated class name, false otherwise.
    hasClass: (name) ->
#if jquery
      @$.hasClass name
#elseif prototype
      @element.hasClassName name
#endif

    # Removes the class name from the element.
    removeClass: (name) ->
#if jquery
      @$.removeClass name
#elseif prototype
      @element.removeClassName name
#endif

      return this

    # Adds the class name to the element.
    addClass: (name) ->
#if jquery
      @$.addClass name
#elseif prototype
      @element.addClassName name
#endif

      return this

    # Updates this element with new content, replacing any old content. The new content may be HTML text, or a DOM
    # element, or an ElementWrapper, or null (to remove the body of the element).
    update: (content) ->
#if jquery
      @$.empty()

      if content
        @$.append (convertContent content)
#elseif prototype
      @element.update (content and convertContent content)
#endif

      return this

    # Appends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
    append: (content) ->
#if jquery
      @$.append (convertContent content)
#elseif prototype
      @element.insert bottom: (convertContent content)
#endif

      return this

    # Prepends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
    prepend: (content) ->
#if jquery
      @$.prepend (convertContent content)
#elseif prototype
      @element.insert top: (convertContent content)
#endif

      return this

    # Inserts new content (Element, ElementWrapper, or HTML markup string) into the DOM immediately before
    # this ElementWrapper's element.
    insertBefore: (content) ->
#if jquery
      @$.before (convertContent content)
#elseif prototype
      @element.insert before: (convertContent content)
#endif

      return this

    # Inserts new content (Element, ElementWrapper, or HTML markup string) into the DOM immediately after
    # this ElementWrapper's element.
    insertAfter: (content) ->
#if jquery
      @$.after (convertContent content)
#elseif prototype
      @element.insert after: (convertContent content)
#endif

      return this

    # Finds the first child element that matches the CSS selector, wrapped as an ElementWrapper.
    # Returns null if not found.
    findFirst: (selector) ->
#if jquery
      match = @$.find selector

      if match.length
        # At least one element was matched, just keep the first
        new ElementWrapper match.first()
#elseif prototype
      match = @element.down selector

      # Prototype returns undefined if not found, we want to return null.
      if match
        new ElementWrapper match
#endif
      else
        return null

    # Finds _all_ child elements matching the CSS selector, returning them
    # as an array of ElementWrappers.
    find: (selector) ->
#if jquery
      matches = @$.find selector

      for i in [0...matches.length]
        new ElementWrapper matches.eq i
#elseif prototype
      matches = @element.select selector

      new ElementWrapper(e) for e in matches
#endif

    # Find the first container element that matches the selector (wrapped as an ElementWrapper),
    # or returns null.
    findParent: (selector) ->
#if jquery
      parents = @$.parents selector

      return null unless parents.length

      new ElementWrapper parents.eq(0)
#elseif prototype
      parent = @element.up selector

      return null unless parent

      new ElementWrapper parent
#endif

    # Returns this ElementWrapper if it matches the selector; otherwise, returns the first container element (as an ElementWrapper)
    # that matches the selector. Returns null if no container element matches.
    closest: (selector) ->
#if jquery
      match = @$.closest selector

      switch
        when match.length is 0 then return null
        when match[0] is @element then return this
        else return new ElementWrapper match
#elseif prototype
      if @element.match selector
        return this

      return @findParent selector
#endif

    # Returns an ElementWrapper for this element's containing element.
    # Returns null if this element has no parent (either because this element is the document object, or
    # because this element is not yet attached to the DOM).
    parent: ->
#if jquery
      parent = @$.parent()

      return null unless parent.length
#elseif prototype
      parent = @element.parentNode

      return null unless parent
#endif
      new ElementWrapper parent

    # Returns an array of all the immediate child elements of this element, as ElementWrappers.
    children: ->
#if jquery
      children = @$.children()

      for i in [0...children.length]
        new ElementWrapper children.eq i

#elseif prototype
      new ElementWrapper(e) for e in @element.childElements()
#endif

    # Returns true if this element is visible, false otherwise. This does not check to see if all containers of the
    # element are visible.
    visible: ->
#if jquery
      @$.css("display") isnt "none"
#elseif prototype
      @element.visible()
#endif

    # Returns true if this element is visible, and all parent elements are also visible, up to the document body.
    deepVisible: ->
      element = this.element
      element.offsetWidth > 0 && element.offsetHeight > 0

    # Fires a named event, passing an optional _memo_ object to event handler functions. This must support
    # common native events (exact list TBD), as well as custom events (in Prototype, custom events must have
    # a prefix that ends with a colon).
    #
    # * eventName - name of event to trigger on the wrapped Element
    # * memo - optional value assocated with the event; available as WrappedeEvent.memo in event handler functions (must
    #   be null for native events). The memo, when provided, should be an object; it is an error if it is a string or other
    #  non-object type..
    #
    # Returns true if the event fully executed, or false if the event was canceled.
    trigger: (eventName, memo) ->
      throw new Error "Attempt to trigger event with null event name" unless eventName?

      unless (_.isNull memo) or (_.isObject memo) or (_.isUndefined memo)
        throw new Error "Event memo may be null or an object, but not a simple type."

#if jquery
      jqEvent = $.Event eventName

      @$.trigger jqEvent, memo

      return not jqEvent.isImmediatePropagationStopped()
#elseif prototype
      if (eventName.indexOf ':') > 0
        # Custom event is supported directly by Prototype:
        event = @element.fire eventName, memo
        return not event.defaultPrevented

      # Native events take some extra work:
      if memo
        throw new Error "Memo must be null when triggering a native event"

      fireNativeEvent @element, eventName
#endif

    # With no parameters, returns the current value of the element (which must be a form control element, such as `<input>` or
    # `<textarea>`). With one parameter, updates the field's value, and returns the previous value. The underlying
    # foundation is responsible for mapping this correctly based on the type of control element.
    # TODO: Define behavior for multi-named elements, such as `<select>`.
    #
    # * newValue - (optional) new value for field
    value: (newValue) ->
#if jquery
      current = @$.val()

      if arguments.length > 0
        @$.val newValue
#elseif prototype
      current = @element.getValue()

      if arguments.length > 0
        @element.setValue newValue
#endif
      return current

    # Returns true if element is a checkbox and is checked
    checked: ->
      @element.checked

    # Stores or retrieves meta-data on the element. With one parameter, the current value for the name
    # is returned (or undefined). With two parameters, the meta-data is updated and the previous value returned.
    # For Prototype, the meta data is essentially empty (except, perhaps, for some internal keys used to store
    # event handling information).  For jQuery, the meta data may be initialized from data- attributes.
    #
    # * name - name of meta-data value to store or retrieve
    # * value - (optional) new value for meta-data
    meta: (name, value) ->
#if jquery
      current = @$.data name

      if arguments.length > 1
        @$.data name, value
#elseif prototype
      current = @element.retrieve name

      if arguments.length > 1
        @element.store name, value
#endif
      return current

    # Adds an event handler for one or more events.
    #
    # * events - one or more event names, separated by spaces
    # * match - optional: CSS expression used as a filter; only events that bubble
    #   up to the wrapped element from an originating element that matches the CSS expression
    #   will invoke the handler.
    # * handler - function invoked; the function is passed an `EventWrapper` object, and the
    #   context (`this`) is the `ElementWrapper` for the matched element.
    #
    # Returns a function of no parameters that removes any added handlers.
    on: (events, match, handler) ->
      exports.on @element, events, match, handler
      return this

    # Returns the text of the element (and its children).
    text: ->
#if jquery
      @$.text()
#elseif prototype
      @element.textContent or @element.innerText
#endif

#if jquery
  # Wrapper around the `jqXHR` object
  class RequestWrapper

    constructor: (@jqxhr) ->

    # Abort a running ajax request
    abort: -> @jqxhr.abort()
#elseif prototype
  # Wrapper around the Prototype `Ajax.Request` object
  class RequestWrapper

    constructor: (@req) ->

    # Abort a running ajax request
    abort: -> throw "Cannot abort Ajax request when using Prototype."
#endif


#if jquery
  # Wrapper around the `jqXHR` object
  class ResponseWrapper

    constructor: (@jqxhr, data) ->

      @status = @jqxhr.status
      @statusText = @jqxhr.statusText
      @json = data # Mostly right?  Need a content type check?
      @text = @jqxhr.responseText

    # Retrieves a response header by name
    header: (name) ->
      @jqxhr.getResponseHeader name
#elseif prototype
  # Wrapper around the Prototype `Ajax.Response` object
  class ResponseWrapper

    constructor: (@res) ->

      @status = @res.status
      @statusText = @res.statusText
      @json = @res.responseJSON
      @text = @res.responseText

    # Retrieves a response header by name
    header: (name) ->
      @res.getHeader name
#endif

  # Used to track how many active Ajax requests are currently in-process. This is incremented
  # when an Ajax request is started, and decremented when an Ajax request completes or fails.
  # The body attribute `data-ajax-active` is set to the number of active Ajax requests, whenever the
  # count changes. This only applies to Ajax requests that are filtered through the t5/core/dom API;
  # other libraries (including RequireJS) which bypass this API are not counted.

  activeAjaxCount = 0

  adjustAjaxCount = (delta) ->
    activeAjaxCount += delta

    exports.body.attr "data-ajax-active", activeAjaxCount

  # Performs an asynchronous Ajax request, invoking callbacks when it completes.
  #
  # This is very low level; most code will want to go through the `t5/core/ajax` module instead,
  # which adds better handling of exceptions and failures, and handles Tapestry's partial page
  # render reponse keys.
  #
  # * options.method - "post", "get", etc., default: "post".
  # * options.contentType - request content, defaults to "application/x-www-form-urlencoded"
  # * options.data - optional, additional key/value pairs (for the default content type)
  # * options.success - handler to invoke on success. Passed the ResponseWrapper object.
  #   Default does nothing.
  # * options.failure - handler to invoke on failure (server responds with a non-2xx code).
  #   Passed the response. Default will throw the exception
  # * options.exception - handler to invoke when an exception occurs (often means the server is unavailable).
  #   Passed the exception. Default will generate an exception message and throw an `Error`.
  #   Note: not really supported under jQuery, a hold-over from Prototype.
  # Returns the module's exports
  ajaxRequest = (url, options = {}) ->
#if jquery
    jqxhr = $.ajax
      url: url
      type: options.method?.toUpperCase() or "POST"
      contentType: options.contentType
      traditional: true
      data: options.data
      # jQuery doesn't have the equivalent of Protoype's onException
      error: (jqXHR, textStatus, errorThrown) ->
        adjustAjaxCount -1

        return if textStatus is "abort"
        message = "Request to #{url} failed with status #{textStatus}"
        text = jqXHR.statusText
        if not _.isEmpty text
          message += " -- #{text}"
        message += "."

        if options.failure
          options.failure (new ResponseWrapper jqXHR), message
        else
          throw new Error message

        return

      success: (data, textStatus, jqXHR) ->

        adjustAjaxCount -1

        options.success and options.success(new ResponseWrapper jqXHR, data)
        return

    adjustAjaxCount +1

    new RequestWrapper jqxhr
#elseif prototype
    finalOptions =
      method: options.method or "post"
      contentType: options.contentType or "application/x-www-form-urlencoded"
      parameters: options.data
      onException: (ajaxRequest, exception) ->

        adjustAjaxCount -1

        if options.exception
          options.exception exception
        else
          throw exception

        return

      onFailure: (response) ->
        adjustAjaxCount -1

        message = "Request to #{url} failed with status #{response.getStatus()}"
        text = response.getStatusText()
        if not _.isEmpty text
          message += " -- #{text}"
        message += "."

        if options.failure
          options.failure (new ResponseWrapper response), message
        else
          throw new Error message

        return

      onSuccess: (response) ->

        adjustAjaxCount -1

        # Prototype treats status == 0 as success, even though it may
        # indicate that the server didn't respond.
        if (not response.getStatus()) or (not response.request.success())
          finalOptions.onFailure(new ResponseWrapper response)
          return

        # Tapestry 5.3 includes lots more exception catching ... that just got in the way
        # of identifying the source of problems.  That's been stripped out.
        options.success and options.success(new ResponseWrapper response)
        return

    adjustAjaxCount +1

    new RequestWrapper (new Ajax.Request url, finalOptions)
#endif

  scanners = null

  # Sets up a scanner callback; this is used to perfom one-time setup of elements
  # that match a particular CSS selector. The callback is passed each element that
  # matches the selector. The callback is expected to modify the element so that it does not
  # match future selections caused by zone updates, typically by removing the CSS class or data- attribute
  # referenced by the selector.
  scanner = (selector, callback) ->
    # Define a function that scans some root element (the body initially; later an updated Zone)
    scan = (root) ->
      callback el for el in root.find selector
      return

    # Do it once immediately:

    scan exports.body

    # Lazily set up a single event handler for running any added scanners.

    if scanners is null
      scanners = []
      exports.body.on events.initializeComponents, ->
        f this for f in scanners
        return

    scanners.push scan

    return

  # The main export is a function that wraps a DOM element as an ElementWrapper; additional functions are attached as
  # properties.
  #
  # * element - a DOM element, or a string id of a DOM element
  #
  # Returns the ElementWrapper, or null if no element with the id exists
  exports = wrapElement = (element) ->
    if _.isString element
#if jquery
      element = document.getElementById element
#elseif prototype
      element = $ element
#endif
      return null unless element
    else
      throw new Error "Attempt to wrap a null DOM element" unless element

#if jquery
    # Assume the object is a DOM element, document or window; something that is compatible with the
    # jQuery API (especially with respect to events).
    new ElementWrapper ($ element)
#elseif prototype
    # Assume the object is a DOM element, document or window; something that is compatible with the
    # Prototype API (especially with respect to events).
    new ElementWrapper element
#endif

  # Creates a new element, detached from the DOM.
  #
  # * elementName - (string) name of element to create, if ommitted, then "div"
  # * attributes - (object) attributes to apply to the created element (may be omitted)
  # * body - (string) content for the new element, may be omitted for no body
  createElement = (elementName, attributes, body) ->

    if _.isObject elementName
      body = attributes
      attributes = elementName
      elementName = null

    if _.isString attributes
      body = attributes
      attributes = null

    element = wrapElement document.createElement (elementName or "div")

    if attributes
      element.attr attributes

    if body
      element.update body

    return element

  _.extend exports,
    wrap: wrapElement

    create: createElement

    ajaxRequest: ajaxRequest

    # Used to add an event handler to an element (possibly from elements below it in the hierarchy).
    #
    # * selector - CSS selector used to select elements to attach handler to; alternately,
    #   a single DOM element, or an array of DOM elements. The document is considered an element
    #   for these purposes.
    # * events - one or more event names, separated by spaces
    # * match - optional: CSS expression used as a filter; only events that bubble
    # * up to a selected element from an originating element that matches the CSS expression
    #   will invoke the handler.
    # * handler - function invoked; the function is passed an `EventWrapper` object, and the context (`this`)
    #   is the `ElementWrapper` for the matched element
    #
    # Returns a function of no parameters that removes any added handlers.
    on: (selector, events, match, handler) ->
      unless handler?
        handler = match
        match = null

#if jquery
      elements = $ selector
#elseif prototype
      elements = parseSelectorToElements selector
      events = utils.split events
#endif
      onevent elements, events, match, handler

    # onDocument() is used to add an event handler to the document object; this is used
    # for global (or default) handlers.
    # Returns a function of no parameters that removes any added handlers.
    onDocument: (events, match, handler) ->
      exports.on document, events, match, handler

    # Returns a wrapped version of the document.body element. Because all Tapestry JavaScript occurs
    # inside a block at the end of the document, inside the `<body`> element, it is assumed that
    # it is always safe to get the body.
    body: wrapElement document.body

    scanner: scanner

  return exports
