You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by hl...@apache.org on 2012/12/26 22:41:26 UTC

[4/6] More modules under a "t5/" folder umbrella e.g. "core/dom" is now "t5/core/dom"

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/dom.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/dom.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/dom.coffee
new file mode 100644
index 0000000..352c2d7
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/dom.coffee
@@ -0,0 +1,538 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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.  This implementation is specific
+# to Prototype, but Tapestry can be adapted to any infrastreucture framework by re-implementing this module.
+#
+# 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.
+define ["_", "./utils", "prototype"], (_, utils) ->
+
+  # 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 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.constructor?.name is "ElementWrapper"
+      return content.element
+
+    throw new Error "Provided value <#{content}> is not valid as DOM element content."
+
+  # Currently don't want to rely on Scriptaculous, since our needs are pretty minor.
+  animate = (element, styleName, initialValue, finalValue, duration, callbacks) ->
+    styles = {}
+    range = finalValue - initialValue
+    initialTime = Date.now()
+    first = true
+    animator = ->
+      elapsed = Date.now() - initialTime
+      if elapsed >= duration
+        styles[styleName] = finalValue
+        element.setStyle styles
+        window.clearInterval timeoutID
+        callbacks.oncomplete and callbacks.oncomplete()
+
+      # TODO: Add an easein/easeout function
+
+      newValue = initial + range * (elapsed / duration)
+
+      element.setStyle styles
+
+      if first
+        callbacks.onstart and callbacks.onstart()
+        first = false
+
+    timeoutID = window.setInterval animator
+
+    styles[styleName] = initialValue
+    element.setStyle styles
+
+  # 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
+
+    constructor: (event) ->
+      @nativeEvent = event
+
+      # 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 ["memo", "type", "char", "key"]
+
+    # Stops the event which prevents further propagation of the DOM event,
+    # as well as DOM event bubbling.
+    stop: ->
+      @nativeEvent.stop()
+
+  # 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 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()`.
+
+  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
+
+        result = handler.call elementWrapper, eventWrapper, eventWrapper.memo
+
+        # If an event handler returns exactly false, then stop the event.
+        if result is false
+          prototypeEvent.stop()
+
+        return
+
+      for element in elements
+        for eventName in eventNames
+          Event.on element, eventName, match, wrapped
+
+      return
+
+  # Wraps a DOM element, providing some common behaviors.
+  # Exposes the original element as property `element`.
+  class ElementWrapper
+
+  # Passed the DOM Element
+    constructor: (@element) ->
+
+      # Hides the wrapped element, setting its display to 'none'.
+    hide: ->
+      @element.hide()
+
+      return this
+
+    # Displays the wrapped element if hidden.
+    show: ->
+      @element.show()
+
+      return this
+
+    # Removes the wrapped element from the DOM.  It can later be re-attached.
+    remove: ->
+      @element.remove()
+
+      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
+    attribute: (name, value) ->
+
+      if _.isObject name
+        for name, value of name
+            @element.writeAttribute name, value
+        return this
+
+      current = @element.readAttribute name
+      if arguments.length > 1
+        @element.writeAttribute name, value
+
+      return current
+
+    # Moves the cursor to the field.
+    focus: ->
+      @element.focus()
+
+      return this
+
+    # Returns true if the element has the indicated class name, false otherwise.
+    hasClass: (name) ->
+      @element.hasClassName name
+
+    # Removes the class name from the element.
+    removeClass: (name) ->
+      @element.removeClassName name
+
+      return this
+
+    # Adds the class name to the element.
+    addClass: (name) ->
+      @element.addClassName name
+
+      return this
+
+    # Updates this element with new content, replacing any old content. The new content may be HTML text, or a DOM
+    # element, or null (to remove the body of the element). Passing null will remove all content.
+    update: (content) ->
+      @element.update (content and convertContent content)
+
+      return this
+
+    # Appends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
+    append: (content) ->
+      @element.insert bottom: (convertContent content)
+
+      return this
+
+    # Prepends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
+    prepend: (content) ->
+      @element.insert top: (convertContent content)
+
+      return this
+
+    # Inserts new content (Element, ElementWrapper, or HTML markup string) into the DOM immediately before
+    # this ElementWrapper's element.
+    insertBefore: (content) ->
+      @element.insert before: (convertContent content)
+
+      return this
+
+    # Inserts new content (Element, ElementWrapper, or HTML markup string) into the DOM immediately after
+    # this ElementWrapper's element.
+    insertAfter: (content) ->
+      @element.insert after: (convertContent content)
+
+      return this
+
+    # Runs an animation to fade-in the element over the specified duration. The element may be hidden (via `hide()`)
+    # initially, and will be made visible (with initial opacity 0, which will increase over time) when the animation
+    # starts.
+    #
+    # * duration - animation duration time, in seconds
+    # * callback - function invoked after the animation is complete
+    fadeIn: (duration, callback) ->
+      animate @element, "opacity", 0, 1, duration * 1000,
+        onstart: => @element.show()
+        oncomplete: callback
+
+      return this
+
+    # Runs an animation to fade out an element over the specified duration. The element should already
+    # be visible and fully opaque.
+    #
+    # * duration - animation duration time, in seconds
+    # * callback - function invoked after the animation is complete
+    fadeOut: (duration, callback) ->
+      animate @element, "opacity", 1, 0, duration * 1000,
+        oncomplete: callback
+
+      return this
+
+    # Finds the first child element that matches the CSS selector, wrapped as an ElementWrapper.
+    # Returns null if not found.
+    findFirst: (selector) ->
+      match = @element.down selector
+
+      # Prototype returns undefined if not found, we want to return null.
+      if match
+        new ElementWrapper match
+      else
+        return null
+
+    # Finds _all_ child elements matching the CSS selector, returning them
+    # as an array of ElementWrappers.
+    find: (selector) ->
+      matches = @element.select selector
+
+      _.map matches, (e) -> new ElementWrapper e
+
+    # Find the first container element that matches the selector (wrapped as an ElementWrapper),
+    # or returns null.
+    findContainer: (selector) ->
+      container = @element.up selector
+
+      if container
+        return new ElementWrapper container
+      else
+        return null
+
+    # Returns an ElementWrapper for this element's containing element.  The ElementWrapper is created lazily, and
+    # cached. Returns null if this element has no parentNode (either because this element is the document object, or
+    # because this element is not yet attached to the DOM).
+    container: ->
+      parentNode = @element.parentNode
+      return null unless parentNode
+
+      new ElementWrapper(parentNode)
+
+    # Returns true if this element is visible, false otherwise. This does not check to see if all containers of the
+    # element are visible.
+    visible: ->
+      @element.visible()
+
+    # Returns true if this element is visible, and all parent elements are also visible, up to the document body.
+    #
+    # Note that in Tapestry 5.3, the search would stop at the nearest form element, not the document body.
+    deepVisible: ->
+      cursor = this
+      while cursor
+        return false unless cursor.visible()
+        cursor = cursor.container()
+
+        return true if cursor and cursor.element is document.body
+
+      # Bound not reached, meaning that the Element is not currently attached to the DOM.
+      return false
+
+    # 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 (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
+
+    # 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) ->
+      current = @element.getValue()
+
+      if arguments.length > 0
+        @element.setValue newValue
+
+      return current
+
+    # 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) ->
+      current = @element.retrieve name
+
+      if arguments.length > 1
+        @element.store name, value
+
+      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.
+    on: (events, match, handler) ->
+      exports.on @element, events, match, handler
+      return this
+
+  # 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]
+
+  bodyWrapper = null
+
+  # 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".
+  #   Adds a "_method" parameter and uses "post" to handle "delete", etc.
+  # * options.contentType - default "context "application/x-www-form-urlencoded"
+  # * options.parameters - optional, additional key/value pairs
+  # * options.onsuccess - handler to invoke on success. Passed the XMLHttpRequest transport object.
+  #   Default does nothing.
+  # * options.onfailure - handler to invoke on failure (server responds with a non-2xx code).
+  #   Passed the response. Default will throw the exception
+  # * options.onexception - 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`.
+  #
+  # TODO: Clarify what the response object looks like and/or wrap the Prototype Ajax.Response object.
+  # TODO: Define what the return value is, or return exports
+  ajaxRequest = (url, options = {}) ->
+    finalOptions =
+      method: options.method or "post"
+      contentType: options.contentType or "application/x-www-form-urlencoded"
+      parameters: options.parameters or {}
+      onException: (ajaxRequest, exception) ->
+        if options.onexception
+          options.onexception exception
+        else
+          throw exception
+
+      onFailure: (response) ->
+        if options.onfailure
+          options.onfailure response
+        else
+          message = "Request to #{url} failed with status #{response.getStatus()}"
+          text = response.getStatusText()
+          if not _.isEmpty text
+            message += " -- #{text}"
+          message += "."
+
+          if options.onfailure
+            options.onfailure response, message
+          else
+            throw new Error message
+
+      onSuccess: (response) ->
+
+        # 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(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.
+        # Still sorting out how this will all work, especially in terms
+        # of the abstraction.
+        options.onsuccess and options.onsuccess(response)
+
+
+    # TODO: This is not good; we need yet another wrapper of some kind here.
+    new Ajax.Request(url, finalOptions)
+
+
+  # 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
+      element = $ element
+      return null unless element
+    else
+      throw new Error "Attempt to wrap a null DOM element" unless element
+
+    # 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
+
+  _.extend exports,
+    wrap: wrapElement
+
+    # Escape's HTML markup in the string.
+    escapeHTML: (str) -> str.escapeHTML()
+
+    ajaxRequest: ajaxRequest
+
+    # Used to add an event handler to an element (possibly from elements below it in the hierarch).
+    #
+    # * 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
+    on: (selector, events, match, handler) ->
+      unless handler?
+        handler = match
+        match = null
+
+      elements = parseSelectorToElements selector
+
+      onevent elements, (utils.split events), match, handler
+      return
+
+    # onDocument() is used to add an event handler to the document object; this is used
+    # for global (or default) 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: ->
+      bodyWrapper ?= (wrapElement document.body)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/events.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/events.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/events.coffee
new file mode 100644
index 0000000..cc2a9bc
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/events.coffee
@@ -0,0 +1,158 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/events
+#
+# This module defines logical names for all events that Tapestry-controlled elements
+# trigger or listener for. Prototype requires that all custom events have a namespace prefix; jQuery appears to
+# allow it without issue.
+define
+  # Defines events related to the validation and submission of forms. See module `t5/core/forms` for further details.
+  # All events are triggered on a specific HTML `<form>` element, and top-level handlers take it from there.
+  form:
+    # Triggered after fields have been validated, when there are no field validation exceptions, to allow for
+    # cross-form validation.
+    validate: "t5:form:validate"
+
+    # Triggered after `validate` (when there are no prior validation exceptions), to allow certain elements
+    # to configure themselves immediately before the form is submitted. This exists primarily for components such
+    # as FormFragment, which will enable or disable a hidden field to match the visibility of the fragment.
+    # There is no event memo.
+    prepareForSubmit: "t5:form:prepare-for-submit"
+
+  # Events releated to form input fields. Primarily, these events are related to form input validation.
+  # Validating a field involves three major steps:
+  #
+  # * optional - check for a required field that has no value
+  # * translate - translate a string to another representation, such as `Date`, or a number
+  # * validate - validate the field against any number of other constraints (such as ranges)
+  #
+  # A field that is blank but not required is considered valid: the translate and validate steps are skipped.
+  #
+  # When a validation error occurs, the event handler should present the validation error (see below), but also
+  # return `false`. This will prevent the event from propogating to other event handlers (Tapestry only supports
+  # a single validation exception per field).
+  #
+  # Presenting validation error: The event handler has two options for indicating a validation failure
+  # at any of the three steps:
+  #
+  # * set the `error` property of the memo to true, and trigger the `showValidationError` event (or otherwise
+  #   make the validation error visible)
+  # * set the `error` property of the memo to the message to display; this will indicate a failure, and the
+  #   `showValidationError` event will be triggered automatically.
+  # * In addition, return `false` to prevent the event bubbling (see note above).
+  field:
+
+    # Perform the optionality check. The event memo includes a `value` property. If the field is required
+    # but the value is blank, then a validation error should be presented (as described above).
+    optional: "t5:field:optional"
+
+    # Trigged by the field if there is a field value. The event memo includes the value as the `value` property.
+    # An event handler may update the event, setting the `translated` property to an alternate formatting, or
+    # alternate representation (e.g., `Date`, or a number) for the input value. If the input can not be translated,
+    # then a validation error should be presented (as described above).
+    translate: "t5:field:translate"
+
+    # Triggered by the field if there is a field value, and the `translate` event succeeded. The event memo
+    # includes a `value' property, and a `translated` property. If any constraints on the field are invalid,
+    # then the event handler should be presented (as described above).
+    validate: "t5:field:validate"
+
+    # Triggered by the form on all enclosed elements with the `data-validation` attribute (indicating they are
+    # interested in participating with user input validation). The default implementation fires a series of
+    # events: `optional`, `translate`, `validate`. The latter two are always skipped if the input is blank, or if
+    # a preceding event set the memo's `error` property to true.  If all events complete without setting an error,
+    # then the `clearValidationError` event is triggered, so remove any validation errors from previous
+    # validation cycles.
+    #
+    # This event is passed a memo object; it should set the memo's `error` property to true if validation failed
+    # for the field.
+    inputValidation: "t5:field:input-validation"
+
+    # Clears and hides the element used to display validation error messages. There is no memo for
+    # this event. The p.help-block for the field is located (if it exists) and empties and hidden.
+    # The containing .control-group element (if it exists) has its "error" class name removed.
+    clearValidationError: "t5:field:clear-validation-error"
+
+    # Presents a validation error for a field. The event memo should have a `message` key; the message to present
+    # (as a string, or even as a detached DOM element). The help block for the field will be located or created,
+    # made visible, and have its content updated to `memo.message`.  If a containing element has the class ".control-group",
+    # then the class "error" will be added.
+    #
+    # The rules for locating the help block:
+    #
+    # * Search for element with attribute `data-error-block-for` set to the field's `id` attribute
+    # * If not found, find the enclosing .controls or .control-group element
+    # * Search enclosing element for an element with attribute `data-presentation="error"`
+    # * Otherwise, it is not found (but may be created dynamically)
+    # * If found, set the `data-error-block-for` attribute to the field's `id` (assigning a unique id to the field
+    #   if not already present)
+    #
+    # The rules for creating the help block:
+    #
+    # * The element is created as `p.help-block` with `data-error-block-for` attribute set to the
+    #   field's id.  The field will be assigned an id if necesary.
+    # * Normally, the block is inserted after the field
+    # * If the field's container has class "input-append" or "input-prepend", then the block is inserted after the container
+    showValidationError: "t5:field:show-validation-error"
+
+  # Defines a number of event names specific to Tapestry Zones. Zones are Tapestry components that are structured
+  # to correctly support dynamic updates from the server via an Ajax request, and a standard response
+  # (the partial page render reponse). More details are available in the `t5/core/zone` module.
+  zone:
+    # Invoked on a zone element to force an update to its content. The event memo should contain a `content` key (an
+    # Element, or a `t5/core/dom:ElementWrapper`, or more typically, a string containing HTML markup). A standard top-level
+    # handler is defined by module `t5/core/zone`, and is responsible for the actual update; it triggers the
+    # `events.zone.willUpdate` and `events.zone.didUpdate` events just before and just after changing the element's
+    # content.
+    update: "t5:zone:update"
+
+    # Triggered (by the standard handler) just before the content in a Zone will be updated.
+    willUpdate: "t5:zone:will-update"
+
+    # Triggered (by the standard hanndler) just after the content in a Zone has updated. If the zone was not visible, it
+    # is made visible after its content is changed, and before this event is triggered. Some number of other components that
+    # also perform Ajax updates of the page also trigger this event.
+    #
+    # Certain components bind this event to scan new additions to the page to see if certain structures exist and
+    # create client-side support in the form of controllers and event handlers. DateField is one such example
+    # (see `t5/core/datefield` module).
+    didUpdate: "t5:zone:did-update"
+
+    # Triggered on a zone element, the default handler will peform an Ajax request and, when the response is available,
+    # update the zone (via `events.zone.update`). The request should provide a partial page render response. If the
+    # response includes a `content` key, its value will be the markup to replace the zone element's body.
+    #
+    # * memo.url - URL to use for the Ajax request
+    # * memo.parameters - (optional) additional query parameters for the request
+    refresh: "t5:zone:refresh"
+
+  # Event names for arbitrary elements. These notifications exist primarily to allow for customizations in how
+  # certain behaviors are presented, for example, to add animation when certain elements are hidden or revealed.
+  element:
+    # Triggered when a hidden element has just been displayed.
+    didShow: "t5:element:did-show"
+    # Trigered when a visible element has just been hidden.
+    didHide: "t5:element:did-hide"
+  # Event names specific to client-side element associated with the FormFragment component. These events exist to allow
+  # client code to cleanly adjust the visibility of the fragment, or remove it.
+  formfragment:
+    # Requests that the fragment change its visibility. The event memo is an object with a single key, visible, a
+    # boolean. The fragment will show or hide itself if necessary (triggering the `element.didShow` or
+    # `element.didHide` event).
+    changeVisibility: "t5:fragment:change-visibility"
+    # Requests that the fragment remove itself entirely. This event is of no practical use, as it is simply equivalent
+    # to invoking `dom/ElementWrapper.remove()` on the fragment's element; the event exists for compatibility with
+    # Tapestry 5.3 and will be removed in Tapestry 5.5.
+    remove: "t5:fragment:remove"

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exception-display.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exception-display.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exception-display.coffee
new file mode 100644
index 0000000..01ba0a3
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exception-display.coffee
@@ -0,0 +1,30 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/exception-report
+#
+# Provides dynamic behavior for the t5/core/ExceptionDisplay component; specifically,
+# filtering the stack trace.
+define ["./dom"],
+  (dom) ->
+
+    dom.onDocument "click", "[data-behavior=stack-trace-filter-toggle]", ->
+      checked = this.element.checked
+
+      for traceList in dom.body().find "ul.t-stack-trace"
+        traceList[if checked then "addClass" else "removeClass"] "t-filtered"
+
+      return
+
+    return null

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exceptionframe.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exceptionframe.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exceptionframe.coffee
new file mode 100644
index 0000000..1993935
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/exceptionframe.coffee
@@ -0,0 +1,61 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/exceptionframe
+#
+# Manages a special element used to present a HTML exception report from an Ajax request (where a non-markup response
+# was expected, including a partial page render response).
+define ["./dom", "./builder", "_"],
+  (dom, builder, _) ->
+    container = null
+    iframe = null
+    iframeDocument = null
+
+    write = (content) ->
+      # Clear current content:
+      iframeDocument.open()
+      # Write new content:
+      iframeDocument.write content
+      iframeDocument.close()
+
+    clear = ->
+      write ""
+      container.hide()
+      return false
+
+    create = ->
+      return if container
+
+      container = builder ".t-exception-container",
+        ["iframe"],
+        ["div > button.pull-right.btn.btn-primary", "Close"]
+
+      dom.body().append container.hide()
+
+      iframe = (container.findFirst "iframe").element
+
+      # See http://xkr.us/articles/dom/iframe-document/
+
+      iframeDocument = iframe.contentWindow or iframe.contentDocument
+      if iframeDocument.document
+        iframeDocument = iframeDocument.document
+
+      container.on "click", "button", clear
+
+    # Export single function:
+
+    (exceptionContent) ->
+      create()
+      write exceptionContent
+      container.show()
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/fields.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/fields.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/fields.coffee
new file mode 100644
index 0000000..795900e
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/fields.coffee
@@ -0,0 +1,164 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/fields
+#
+# Module for logic relating to form input fields (input, select, textarea); specifically
+# presenting validation errors and perfoming input validation when necessary.
+define ["_", "./events", "./dom", "./builder", "./utils", "./forms"],
+  (_, events, dom, builder, utils) ->
+
+    ensureFieldId = (field) ->
+      fieldId = field.attribute "id"
+
+      unless fieldId
+        fieldId = _.uniqueId "field"
+        field.attribute "id", fieldId
+
+      return fieldId
+
+    # Finds a `p.help-block` used for presenting errors for the provided field.
+    # Returns the found block as an ElementWrapper. May modify attributes of the field
+    # or the block to make future
+    #
+    # * field - element wrapper for the field
+    findHelpBlock = (field) ->
+      fieldId = field.attribute "id"
+
+      # When the field has an id (the normal case!), search the body for
+      # the matching help block.
+      if fieldId
+        block = dom.body().findFirst "[data-error-block-for=#{fieldId}]"
+
+        return block if block
+      else
+        # Assign a unique (hopefully!) client id for the field, which will be
+        # used to link the field and the label together.
+        fieldId = ensureFieldId field
+
+      # Not found by id, but see if an empty placeholder was provided within
+      # the same .controls or .control-group.
+
+      group = field.findContainer ".controls, .control-group"
+
+      return null unless group
+
+      block = group.findFirst "[data-presentation=error]"
+
+      if block
+
+        unless fieldId
+          # Assign a unique (hopefully!) client id for the field, which will be
+          # used to link the field and the label together.
+          fieldId = _.uniqueId "field"
+          field.attribute "id", fieldId
+
+        block.attribute "data-error-block-for", fieldId
+
+      return block
+
+    createHelpBlock = (field) ->
+      fieldId = ensureFieldId field
+
+      # No containing group ... this is a problem, probably an old 5.3 application upgraded to 5.4
+      # or beyond.  Place the block just after the field.
+
+      container = field.container()
+
+      block = builder "p.help-block", "data-error-block-for": fieldId
+
+      # The .input-append and .input-prepend are used to attach buttons or markers to the field.
+      # In which case, the block can go
+      if container.hasClass("input-append") or container.hasClass("input-prepend")
+        container.insertAfter block
+      else
+        field.insertAfter block
+
+      return block
+
+    showValidationError = (id, message) ->
+      dom.wrap(id).trigger events.field.showValidationError, { message }
+
+    # Default registrations:
+
+    dom.onDocument events.field.inputValidation, (event, formMemo) ->
+
+      # When not visible to the user, ignore the input validation. Components
+      # are generally configured so that they do not submit a value to the server
+      # when not visible ... this is what the core/FormFragment component is responsible
+      # for.
+      return unless this.deepVisible()
+
+      failure = false
+
+      memo = value: this.value()
+
+      postEventTrigger = =>
+        if memo.error
+          # Assume the event handler displayed the message.
+          failure = true
+
+          if _.isString memo.error
+            this.trigger events.field.showValidationError, { message: memo.error }
+
+      this.trigger events.field.optional, memo
+
+      postEventTrigger()
+
+      unless failure or (utils.isBlank memo.value)
+
+        this.trigger events.field.translate, memo
+
+        postEventTrigger()
+
+        unless failure
+            if _.isUndefined memo.translated
+              memo.translated = memo.value
+
+            this.trigger events.field.validate, memo
+
+            postEventTrigger()
+
+      if failure
+        formMemo.error = true
+      else
+        this.trigger events.field.clearValidationError
+
+      return
+
+    dom.onDocument events.field.clearValidationError, ->
+      block = exports.findHelpBlock this
+
+      if block
+        block.hide().update("")
+
+      group = this.findContainer ".control-group"
+
+      group and group.removeClass "error"
+
+      return
+
+    dom.onDocument events.field.showValidationError, (event, memo) ->
+      block = exports.findHelpBlock this
+
+      unless block
+        block = exports.createHelpBlock this
+
+      block.show().update(memo.message)
+
+      group = this.findContainer ".control-group"
+
+      group and group.addClass "error"
+
+    exports = {findHelpBlock, createHelpBlock, showValidationError}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/form-fragment.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/form-fragment.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/form-fragment.coffee
new file mode 100644
index 0000000..bc288a0
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/form-fragment.coffee
@@ -0,0 +1,80 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/form-fragment
+#
+define ["_", "./dom", "./events", "./forms"],
+  (_, dom, events) ->
+
+    SELECTOR = '[data-component-type="core/FormFragment"]'
+
+    # This is mostly for compatibility with 5.3, which supported
+    # a DOM event to ask a fragment to remove itself.  This makes less sense since
+    # default animations were eliminated in 5.4.
+    dom.onDocument events.formfragment.remove, SELECTOR, (event) ->
+      this.remove()
+
+    # When any form fires the prepareForSubmit event, check to see if
+    # any form fragments are contained within, and give them a chance
+    # to enabled/disable their hidden field.
+    dom.onDocument events.form.prepareForSubmit, "form", (event) ->
+
+      fragments = this.find SELECTOR
+
+      _.each fragments, (frag) ->
+
+        fragmentId = frag.attribute "id"
+
+        hidden = frag.findFirst "input[type=hidden][data-for-fragment=#{fragmentId}]"
+
+        # If found (e.g., not alwaysSubmit), then enable/disable the field.
+        hidden && hidden.attribute "disabled", not frag.deepVisible()
+
+    # Again, a DOM event to make the FormFragment visible or invisible; this is useful
+    # because of the didShow/didHide events ... but we're really just seeing the evolution
+    # from the old style (the FormFragment class as controller) to the new style (DOM events and
+    # top-level event handlers).
+    dom.onDocument events.formfragment.changeVisibility, SELECTOR, (event) ->
+        makeVisible = event.memo.visible
+
+        this[if makeVisible then "show" else "hide"]()
+
+        this.trigger events.element[if makeVisible then "didShow" else "didHide"]
+
+        return false
+
+    # Initializes a trigger for a FormFragment
+    #
+    # * spec.triggerId - id of checkbox or radio button
+    # * spec.fragmentId - id of FormFragment element
+    # * spec.invert - (optional) if true, then checked trigger hides (not shows) the fragment
+    linkTrigger = (spec) ->
+      trigger = dom spec.triggerId
+      invert = spec.invert or false
+
+      update = ->
+        checked = trigger.element.checked
+        makeVisible = checked isnt invert
+
+        (dom spec.fragmentId).trigger events.formfragment.changeVisibility,  visible: makeVisible
+
+        return
+
+      if trigger.element.type is "radio"
+        dom.on trigger.element.form, "click", update
+      else
+        trigger.on "click", update
+
+    { linkTrigger }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/forms.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/forms.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/forms.coffee
new file mode 100644
index 0000000..ba04774
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/forms.coffee
@@ -0,0 +1,167 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/forms
+#
+# Defines handlers for HTML forms and HTML field elements, specifically to control input validation.
+
+define ["./events", "./dom", "./builder", "_"],
+  (events, dom, builder, _) ->
+
+    # Meta-data name that indicates the next submission should skip validation (typically, because
+    # the form was submitted by a "cancel" button).
+    SKIP_VALIDATION = "t5:skip-validation"
+
+    clearSubmittingHidden = (form) ->
+      hidden = form.findFirst "[name='t:submit']"
+
+      # Clear if found
+      hidden and hidden.value null
+
+      form.meta SKIP_VALIDATION, null
+
+      return
+
+    setSubmittingHidden = (form, submitter) ->
+
+      mode = submitter.attribute "data-submit-mode"
+      isCancel = mode is "cancel"
+      if mode and mode isnt "normal"
+        form.meta SKIP_VALIDATION, true
+
+      hidden = form.findFirst "[name='t:submit']"
+
+      unless hidden
+        firstHidden = form.findFirst "input[type=hidden]"
+        hidden = builder "input", type:"hidden", name:"t:submit"
+        firstHidden.insertBefore hidden
+
+      # TODO: Research why we need id and name and get rid of one if possible.
+      name = if isCancel then "cancel" else submitter.element.name
+      value = Object.toJSON [ submitter.element.id, name ]
+
+      hidden.value value
+
+      return
+
+    # Passed the element wrapper for a form element, returns a map of all the values
+    # for all non-disabled fields (including hidden fields, select, textarea). This is primarily
+    # used when assembling an Ajax request for a form submission.
+    gatherParameters = (form) ->
+      result = {}
+
+      fields = form.find "input, select, textarea"
+
+      _.each fields, (field) ->
+          return if field.attribute "disabled"
+
+          type = field.element.type
+
+          # Ignore types file and submit; file doesn't make sense for Ajax, and submit
+          # is handled by keeping a hidden field active with the data Tapestry needs
+          # on the server.
+          return if type is "file" || type is "submit"
+
+          value = field.value()
+
+          return if value is null
+
+          name = field.element.name
+
+          existing = result[name]
+
+          if _.isArray existing
+            existing.push value
+            return
+
+          if existing
+            result[name] = [existing, value]
+            return
+
+          result[name] = value
+
+      return result
+
+
+    defaultValidateAndSubmit = ->
+
+      if ((this.attribute "data-validate") is "submit") and
+         (not this.meta SKIP_VALIDATION)
+
+        this.meta SKIP_VALIDATION, null
+
+        memo = error: false
+
+        for field in this.find "[data-validation]"
+          field.trigger events.field.inputValidation, memo
+
+        # Only do form validation if all individual field validation
+        # was successful.
+        unless memo.error
+          this.trigger events.form.validate, memo
+
+        if memo.error
+          clearSubmittingHidden this
+          # Cancel the original submit event when there's an error
+          return false
+
+      # Allow certain types of elements to do last-moment set up. Basically, this is for
+      # FormFragment, or similar, to make their hidden field enabled or disabled to match
+      # their UI's visible/hidden status. This is assumed to work or throw an exception; there
+      # is no memo.
+      this.trigger events.form.prepareForSubmit
+
+      # Otherwise, the event is good, there are no validation problems, let the normal processing commence.
+      # Possibly, the document event handler provided by the t5/core/zone module will intercept form submission if this
+      # is an Ajax submission.
+      return
+
+    dom.onDocument "submit", "form", defaultValidateAndSubmit
+
+    # On any click on a submit or image, update the containing form to indicate that the element
+    # was responsible for the eventual submit; this is very important to Ajax updates, otherwise the
+    # information about which control triggered the submit gets lost.
+    dom.onDocument "click", "input[type=submit], input[type=image]", ->
+      setSubmittingHidden (dom this.element.form), this
+      return
+
+    # Support for link submits. `data-submit-mode` will be non-null, possibly "cancel".
+    # Update the hidden field, but also cancel the default behavior for the click.
+    dom.onDocument "click", "a[data-submit-mode]", ->
+      form = this.findContainer "form"
+
+      unless form
+        console.error "Submitting link element not contained inside a form element."
+        return false
+
+      setSubmittingHidden form, this
+
+      # Now the ugly part; if we just invoke submit() on the form, it does not trigger
+      # the form's "submit" event, which we need.
+
+      if form.trigger "submit"
+        form.submit()
+
+      # And cancel the default behavior for the original click event
+      return false
+
+    exports =
+      gatherParameters: gatherParameters
+
+      setSubmittingElement: setSubmittingHidden
+
+      # Sets a flag on the form to indicate that client-side validation should be bypassed.
+      # This is typically associated with submit buttons that "cancel" the form.
+      skipValidation: (form) ->
+        form.meta SKIP_VALIDATION, true
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/grid.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/grid.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/grid.coffee
new file mode 100644
index 0000000..5dd02da
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/grid.coffee
@@ -0,0 +1,38 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/grid
+#
+# Adds support for in-place updates of the Grid component.  The Grid renders a
+# div[data-zone] around the table, and code here intercepts clicks on links that
+# are inside a div[data-inplace-grid-links].
+#
+define ["./dom", "./events", "./console"],
+
+  (dom, events, console) ->
+
+    dom.onDocument "[data-inplace-grid-links] a", ->
+
+      zone = this.findContainer "[data-container-type=zone]"
+
+      unless zone
+        console.error "Unable to find containing zone for live update of grid."
+        return false
+
+      zone.trigger events.zone.refresh, url: this.attribute "href"
+
+      return false
+
+    return null

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/init.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/init.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/init.coffee
new file mode 100644
index 0000000..3d4385f
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/init.coffee
@@ -0,0 +1,34 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/init
+#
+# Compatibility module, invokes functions on the T5.initializers namespace.
+#
+# Introduced in 5.4, to be removed at some point in the future, when T5.initializers is itself no more.
+define ["./console"],
+
+  (console) ->
+
+    # Temporary, until we rework the client-side input validation.
+
+    T5.initializers.validate = ->
+
+    # Exports a single function that finds an initializer in `T5.initializers` and invokes it.
+    (initName, args...) ->
+      fn = T5.initializers[initName]
+      if not fn
+        console.error "Initialization function '#{initName}' not found in T5.initializers namespace."
+      else
+        fn.apply null, args

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/messages.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/messages.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/messages.coffee
new file mode 100644
index 0000000..b01e3fc
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/messages.coffee
@@ -0,0 +1,55 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/messages
+#
+# For all of these modules, we've turned off CoffeeScript's normal outer function
+# wrapper, as each module is just a call to `define()` with a function that fulfills
+# the same purpose. This one is different, as it is necessary to compute one of the dependencies.
+# On the server `t5/core/messages/<locale>` is actually generated dynamically, as is a simple
+# mapping of message keys to message values, from the global application message catalog.
+#
+# This module provides access to localized messages from the Tapestry applications' server-side
+# application message catalog (which is, itself, built from multiple resources, some provided by
+# the framework, others provided by the application, or third-party libraries).
+#
+# Messages in the catalog that contain Java-style format specifiers are not included, as there
+# is no facility for formatting those on the client. This is actually done as a simple test for the
+# presence of the `%` character.  In addition, any message key that begins with "private-" is
+# assumed to contain sensitive data (such as database URLs or passwords) and will not be
+# exposed to the client.
+do ->
+  # In the unexpected case that the data-locale attribute is missing, assume English
+  locale = (document.documentElement.getAttribute "data-locale") or "en"
+
+  define ["./messages/#{locale}", "_", "./console"],
+    (messages, _, console) ->
+
+      # Returns the application message catalog message for the given key. Returns
+      # a placeholder if the key is not found.
+      get = (key) ->
+        value = messages[key]
+
+        if value
+          return value
+        else
+          console.error "No value for message catalog key '#{key}' exists."
+          return "[[Missing Key: '#{key}']]"
+
+      # Returns all keys that are defined by the underlying catalog, in no specific order.
+      get.keys = -> _.keys messages
+
+
+      # Export get as the main function.
+      return get

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee
new file mode 100644
index 0000000..e37e765
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee
@@ -0,0 +1,218 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/pageinit
+#
+# Module that defines functions used for page initialization.
+# The initialize function is passed an array of initializers; each initializer is itself
+# an array. The first value in the initializer defines the name of the module to invoke.
+# The module name may also indicate the function exported by the module, as a suffix following a colon:
+# e.g., "my/module:myfunc".
+# Any additional values in the initializer are passed to the function. The context of the function (this) is null.
+define ["_", "./console", "./dom", "./events"],
+  (_, console, dom, events) ->
+    pathPrefix = null
+
+    # Borrowed from Prototype:
+    isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'
+    isIE = !!window.attachEvent && !isOpera
+
+    rebuildURL = (path) ->
+      return path if path.match /^https?:/
+
+      # See Tapestry.rebuildURL() for an error about the path not starting with a leading '/'
+      # We'll assume that doesn't happen.
+
+      if !pathPrefix
+        l = window.location
+        pathPrefix = "#{l.protocol}//#{l.host}"
+
+      return pathPrefix + path
+
+    rebuildURLOnIE =
+      if isIE then rebuildURL else _.identity
+
+    addStylesheets = (newStylesheets) ->
+      return unless newStylesheets
+
+      # Figure out which stylesheets are loaded; adjust for IE, and especially, IE 9
+      # which can report a stylesheet's URL as null (not empty string).
+      loaded = _.chain(document.styleSheets)
+      .pluck("href")
+      .without("")
+      .without(null)
+      .map(rebuildURLOnIE)
+
+      insertionPoint = _.find(document.styleSheets, (ss) -> ss.ownerNode.rel is "stylesheet t-ajax-insertion-point")
+
+      # Most browsers support document.head, but older IE doesn't:
+      head = document.head or document.getElementsByTagName("head")[0]
+
+      _.chain(newStylesheets)
+      .map((ss) -> { href: rebuildURL(ss.href), media: ss.media })
+      .reject((ss) -> loaded.contains(ss.href).value())
+      .each((ss) ->
+        element = document.createElement "link"
+        element.setAttribute "type", "text/css"
+        element.setAttribute "rel", "stylesheet"
+        element.setAttribute "href", ss.href
+        if ss.media
+          element.setAttribute "media", ss.media
+
+        if insertionPoint
+          head.insertBefore element, insertionPoint.ownerNode
+        else
+          head.appendChild element
+
+        console.debug "Added stylesheet #{ss.href}"
+      )
+
+      return
+
+    invokeInitializer = (tracker, qualifiedName, initArguments) ->
+      [moduleName, functionName] = qualifiedName.split ':'
+
+      require [moduleName], (moduleLib) ->
+
+        # Some modules export nothing but do some full-page initialization, such as adding
+        # event handlers to the body.
+        if not functionName and
+          initArguments.length is 0 and
+          not _.isFunction moduleLib
+            console.debug "Loaded module #{moduleName}"
+            tracker()
+            return
+
+        fn = if functionName? then moduleLib[functionName] else moduleLib
+
+        if console.debugEnabled
+          argsString = _.map(initArguments, JSON.stringify).join(", ")
+          console.debug "Invoking #{qualifiedName}(#{argsString})"
+
+        fn.apply null, initArguments
+
+
+        tracker()
+
+    exports =
+      # Passed a list of initializers, executes each initializer in order. Due to asynchronous loading
+      # of modules, the exact order in which initializer functions are invoked is not predictable.
+      initialize: (inits = [], callback) ->
+        console.debug "Executing #{inits.length} inits"
+        callbackCountdown = inits.length + 1
+
+        # tracker gets invoked once after each require/callback, plus once extra
+        # (to handle the case where there are no inits). When the count hits zero,
+        # it invokes the callback (if there is one).
+        tracker = ->
+          callbackCountdown--
+
+          if callbackCountdown is 0
+            console.debug "All inits executed"
+            callback() if callback
+
+        # First value in each init is the qualified module name; anything after
+        # that are arguments to be passed to the identified function. A string
+        # is the name of a module to load, or function to invoke, that
+        # takes no parameters.
+        for init in inits
+          if _.isString init
+            invokeInitializer tracker, init, []
+          else
+            [qualifiedName, initArguments...] = init
+            invokeInitializer tracker, qualifiedName, initArguments
+
+        tracker()
+
+      # Pre-loads a number of libraries in order. When the last library is loaded,
+      # invokes the callback (with no parameters).
+      loadLibraries: (libraries, callback) ->
+        reducer = (callback, library) -> ->
+          console.debug "Loading library #{library}"
+          require [library], callback
+
+        finalCallback = _.reduceRight libraries, reducer, callback
+
+        finalCallback.call null
+
+      # Loads all specified libraries in order (this includes other the core stack, other stacks, and
+      # any free-standing libraries). It then executes the immediate initializations. After that, it waits for the DOM to be
+      # ready (which, given typical Tapestry page structure, it almost certainly is at the point this function
+      # executed), and then executes the other initializations.
+      loadLibrariesAndInitialize: (libraries, inits) ->
+        console.debug "Loading #{libraries?.length or 0} libraries"
+        exports.loadLibraries libraries,
+          -> exports.initialize inits,
+            ->
+              # At this point, all libraries have been loaded, and all inits should have executed. Unless some of
+              # the inits triggered Ajax updates (such as a core/ProgressiveDisplay component), then the page should
+              # be ready to go. We set a flag, mostly used by test suites, to ensure that all is ready.
+              # Note that later Ajax requests do not change this attribute, so their timing continues to be tricky.
+
+              (dom document.documentElement).attribute "data-page-initialized", "true"
+
+      evalJavaScript: (js) ->
+        console.debug "Evaluating: #{js}"
+        eval js
+
+      focus: (fieldId) ->
+        field = dom fieldId
+
+        field && field.focus()
+
+      # Passed the response from an Ajax request, when the request is successful.
+      # This is used for any request that attaches partial-page-render data to the main JSON object
+      # response.  If no such data is attached, the callback is simply invoked immediately.
+      # Otherwise, Tapestry processes the partial-page-render data. This may involve loading some number
+      # of JavaScript libraries and CSS style sheets, and a number of direct updates to the DOM. After DOM updates,
+      # the callback is invoked, passed the response (with any Tapestry-specific data removed).
+      # After the callback is invoked, page initializations occur.  This method returns null.
+
+      # * response - the Ajax response object
+      # * callback - invoked after scripts are loaded, but before page initializations occur (may be null)
+      handlePartialPageRenderResponse: (response, callback) ->
+
+        # Capture the partial page response portion of the overall response, and
+        # then remove it so it doesn't interfere elsewhere.
+        responseJSON = response.responseJSON or {}
+        partial = responseJSON._tapestry
+        delete responseJSON._tapestry
+
+        # Extreme case: the data has a redirectURL which forces an immediate redirect to the URL.
+        # No other initialization or callback invocation occurs.
+        if partial?.redirectURL
+          window.location.href = partial.redirectURL
+          return
+
+        addStylesheets partial?.stylesheets
+
+        # Make sure all libraries are loaded
+        exports.loadLibraries partial?.libraries, ->
+
+          # After libraries are loaded, update each zone:
+          _(partial?.content).each ([id, content]) ->
+            console.debug "Updating content for zone #{id}"
+
+            zone = dom.wrap id
+
+            if zone
+              zone.trigger events.zone.update, { content }
+
+          # Invoke the callback, if present.  The callback may do its own content updates.
+          callback and callback(response)
+
+          # Lastly, perform initializations from the partial page render response.
+          exports.initialize partial?.inits
+
+        return

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/palette.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/palette.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/palette.coffee
new file mode 100644
index 0000000..a4e4174
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/palette.coffee
@@ -0,0 +1,228 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/palette
+#
+# Support for the `core/Palette` component.
+define ["./dom", "_"],
+  (dom, _) ->
+    class PaletteController
+
+      constructor: (id) ->
+        @selected = (dom id)
+        @container = @selected.findContainer ".t-palette"
+        @available = @container.findFirst ".t-palette-available select"
+        @hidden = @container.findFirst "input[type=hidden]"
+
+        @select = @container.findFirst "[data-action=select]"
+        @deselect = @container.findFirst "[data-action=deselect]"
+
+        @moveUp = @container.findFirst "[data-action=move-up]"
+        @moveDown = @container.findFirst "[data-action=move-down]"
+
+        # Track where reorder is allowed based on whether the buttons actually exist
+        @reorder = @moveUp isnt null
+
+        @valueToOrderIndex = {}
+
+        _.each @available.element.options, (option, i) =>
+          @valueToOrderIndex[option.value] = i
+
+        # This occurs even when the palette is disabled, to present the
+        # values correctly. Otherwise it looks like nothing is selected.
+        @initialTransfer()
+
+        unless @selected.element.disabled
+          @updateButtons()
+          @bindEvents()
+
+      initialTransfer: ->
+        # Get the values for options that should move over
+        values = JSON.parse @hidden.value()
+        valueToPosition = {}
+
+        _.each values, (v, i) -> valueToPosition[v] = i
+
+        e = @available.element
+
+        movers = []
+
+        for i in [(e.options.length - 1)..0] by -1
+          option = e.options[i]
+          value = option.value
+          pos = valueToPosition[value]
+          unless pos is undefined
+            movers[pos] = option
+            e.remove i
+
+        for option in movers
+          @selected.element.add option
+
+      updateAfterChange: ->
+        @updateHidden()
+        @updateButtons()
+
+      updateHidden: ->
+        values = _.pluck(@selected.element.options, "value")
+        @hidden.value JSON.stringify values
+
+      bindEvents: ->
+        @container.on "change", "select", =>
+          @updateButtons()
+          return false
+
+        @select.on "click", =>
+          @doSelect()
+          return false
+
+        @available.on "dblclick", =>
+          @doSelect()
+          return false
+
+        @deselect.on "click", =>
+          @doDeselect()
+          return false
+
+        @selected.on "dblclick", =>
+          @doDeselect()
+          return false
+
+        if @reorder
+          @moveUp.on "click", =>
+            @doMoveUp()
+            return false
+
+          @moveDown.on "click", =>
+            @doMoveDown()
+            return false
+
+      updateButtons: ->
+        @select.element.disabled = @available.element.selectedIndex < 0
+
+        nothingSelected = @selected.element.selectedIndex < 0
+
+        @deselect.element.disabled = nothingSelected
+
+        if @reorder
+          @moveUp.element.disabled = nothingSelected or @allSelectionsAtTop()
+          @moveDown.element.disabled = nothingSelected or @allSelectionsAtBottom()
+
+      doSelect: -> @transferOptions @available, @selected, @reorder
+
+      doDeselect: -> @transferOptions @selected, @available, false
+
+      doMoveUp: ->
+        e = @selected.element
+        pos = e.selectedIndex - 1
+        movers = @removeSelectedOptions @selected
+        before = e.options[if pos < 0 then 0 else pos]
+
+        @reorderOptions movers, before
+
+      doMoveDown: ->
+        e = @selected.element
+        lastSelected = _.chain(e.options).toArray().reverse().find((o) -> o.selected).value()
+
+        lastPos = lastSelected.index
+        before = e.options[lastPos + 2]
+
+        movers = @removeSelectedOptions @selected
+
+        @reorderOptions movers, before
+
+      reorderOptions: (movers, before) ->
+        for mover in movers
+          @addOption @selected, mover, before
+        @updateAfterChange()
+
+      transferOptions: (from, to, atEnd) ->
+        if from.element.selectedIndex is -1
+          return
+
+        _(to.element.options).each (o) -> o.selected = false
+
+        movers = @removeSelectedOptions from
+
+        @moveOptions movers, to, atEnd
+
+      removeSelectedOptions: (select) ->
+        movers = []
+        e = select.element
+        options = e.options
+
+        for i in [(e.length - 1)..(e.selectedIndex)] by -1
+          o = options[i]
+          if o.selected
+            e.remove i
+            movers.unshift o
+
+        return movers
+
+      moveOptions: (movers, to, atEnd) ->
+        _.each movers, (o) =>
+          @moveOption o, to, atEnd
+
+        @updateAfterChange()
+
+      moveOption: (option, to, atEnd) ->
+        before = null
+
+        unless atEnd
+          optionOrder = @valueToOrderIndex[option.value]
+          candidate = _.find to.element.options, (o) => @valueToOrderIndex[o.value] > optionOrder
+          if candidate
+            before = candidate
+
+        @addOption to, option, before
+
+      addOption: (to, option, before) ->
+        try
+          to.element.add option, before
+        catch ex
+          if before is null
+            # IE throws an exception about type mismatch; here's the fix:
+            to.add option
+          else
+            to.add option, before.index
+
+      indexOfLastSelection: (select) ->
+        e = select.element
+        if e.selectedIndex < 0
+          return -1
+
+        for i in [(e.options.length - 1)..(e.selectedIndex)] by -1
+          if e.options[i].selected
+            return i
+
+        return -1
+
+      allSelectionsAtTop: ->
+        last = @indexOfLastSelection @selected
+        options = _.toArray @selected.element.options
+
+        _(options[0..last]).all (o) -> o.selected
+
+      allSelectionsAtBottom: ->
+        e = @selected.element
+        last = e.selectedIndex
+        options = _.toArray e.options
+
+        _(options[last..]).all (o) -> o.selected
+
+
+    initialize = (id) ->
+      new PaletteController(id)
+
+    # Export just the initialize function
+    return initialize
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/select.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/select.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/select.coffee
new file mode 100644
index 0000000..fcbfd09
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/select.coffee
@@ -0,0 +1,30 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/select
+#
+# Provides a document event handler that triggers an update a zone when the value
+# of a select element within the zone changes.
+define ["./events", "./dom", "./zone"],
+
+  (events, dom, zone) ->
+
+        dom.onDocument "change", "select[data-update-zone]", ->
+
+          containingZone = zone.findZone this
+
+          containingZone and containingZone.trigger events.zone.refresh,
+            url: this.attribute "data-update-url"
+            parameters:
+              "t:selectvalue": this.value()

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/tree.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/tree.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/tree.coffee
new file mode 100644
index 0000000..ca9897c
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/tree.coffee
@@ -0,0 +1,113 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/tree
+#
+# Handlers to support to the core/Tree Tapestry component
+define ["./dom", "./ajax", "./zone"],
+  (dom, ajax) ->
+    TREE = "[data-component-type=core/Tree]"
+    NODE_ID = "data-node-id"
+    SELECTOR = "#{TREE} [#{NODE_ID}]"
+
+    LOADING = "tree-children-loading"
+    LOADED = "tree-children-loaded"
+    EXPANDED = "t-tree-expanded"
+    SELECTED = "t-selected-leaf-node"
+
+    send = (node, action, onsuccess) ->
+      container = node.findContainer TREE
+      url = container.attribute "data-tree-action-url"
+
+      ajax url,
+        parameters:
+          "t:action": action
+          "t:nodeid": node.attribute NODE_ID
+        onsuccess: onsuccess
+
+    loadChildren = (node) ->
+
+      # Ignore duplicate requests to load the children.
+      return if node.meta LOADING
+
+      node.meta LOADING, true
+
+      node.addClass "t-empty-node"
+      node.update "<span class='t-ajax-wait'/>"
+
+      send node, "expand", (reply) ->
+        # Remove the Ajax spinner and  mark the node as expanded (it will have a "-"
+        # icon instead of a "+" icon)
+        node.update("").addClass(EXPANDED).removeClass("t-empty-node")
+
+        label = node.findContainer("li").findFirst(".t-tree-label")
+
+        label.insertAfter reply.responseJSON.content
+
+        node.meta LOADING, false
+        node.meta LOADED, true
+
+    toggle = (node) ->
+      sublist = node.findContainer("li").findFirst("ul")
+
+      if node.hasClass EXPANDED
+        node.removeClass EXPANDED
+        sublist.hide()
+        send node, "markCollapsed"
+        return
+
+      node.addClass EXPANDED
+      sublist.show()
+      send node, "markExpanded"
+
+    clickHandler = ->
+
+      # First case is dynamically loaded due to user action; second case
+      # is rendered with overall page due to server-side expansion model.
+      loaded = (this.meta LOADED) or (this.hasClass EXPANDED)
+
+      if (not loaded) and (not this.hasClass "t-empty-node")
+        loadChildren this
+        return false
+
+      unless this.hasClass "t-leaf-node"
+        toggle this
+        return false
+
+      return false
+
+    toggleSelection = ->
+
+      selected = this.hasClass SELECTED
+
+      node = this.findContainer("li").findFirst("[#{NODE_ID}]")
+
+      if selected
+        this.removeClass SELECTED
+        send node, "deselect"
+      else
+        this.addClass SELECTED
+        send node, "select"
+
+      return false
+
+    dom.onDocument "click", SELECTOR, clickHandler
+
+    dom.onDocument "click",
+      "#{TREE}[data-tree-node-selection-enabled] LI.t-leaf-node > .t-tree-label",
+      toggleSelection
+
+
+    return null
+  

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/3a7ac53e/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/utils.coffee
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/utils.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/utils.coffee
new file mode 100644
index 0000000..443792d
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/utils.coffee
@@ -0,0 +1,40 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# 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/utils
+#
+# A few handy functions.
+define ["_"], (_) ->
+
+  trim = (input) ->
+    if String.prototype.trim
+      input.trim()
+    else
+      input.replace(/^\s+/, '').replace(/\s+$/, '')
+
+  exports =
+    # Trims leading and trailing whitespace from a string. Delegates to String.prototype.trim if present.
+    trim: trim
+    # Determines if the input is a blank string, or null, or an empty array.
+    isBlank: (input) ->
+
+        return true if input is null
+
+        if _.isArray input
+          return input.length is 0
+
+        return (exports.trim input).length is 0
+
+    # Splits the input string into words separated by whitespace
+    split: (str) -> _(str.split " ").reject((s) -> s is "")