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 "")