define ["underscore", "./utils", "./events", "jquery"],
(_, utils, events, $) ->
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.
This is the abstraction layer that allows the majority of components to operate without caring whether the underlying infrastructure framework is Prototype, jQuery, or something else.
The abstraction layer has a number of disadvantages:
It 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 ["underscore", "./utils", "./events", "jquery"],
(_, utils, events, $) ->
Converts content (provided to ElementWrapper.update()
or append()
) into an appropriate type. This
primarily exists to validate the value, and to “unpack” an ElementWrapper into a DOM element.
convertContent = (content) ->
if _.isString content
return content
if _.isElement content
return content
if content instanceof ElementWrapper
return content.$
throw new Error "Provided value <#{content}> is not valid as DOM element content."
Generic view of an DOM event that is passed to a handler function.
Properties:
ElementWrapper.trigger()
.char
property for printable keys,
or a key name for others. class EventWrapper
constructor: (event, memo) ->
@nativeEvent = event
@memo = memo
This is to satisfy YUICompressor which doesn’t seem to like ‘char’, even though it doesn’t appear to be a reserved word.
this[name] = event[name] for name in ["type", "char", "key"]
Stops the event which prevents further propagation of the DOM event, as well as DOM event bubbling.
stop: ->
@nativeEvent.preventDefault()
@nativeEvent.stopImmediatePropagation()
Interface between the dom’s event model, and jQuery’s.
EventWrapper
instance as the first parameter,
and the memo as the second parameter. this
will be the ElementWrapper
for the matched element.Event handlers may return false to stop event propogation; this prevents an event from bubbling up, and
prevents any browser default behavior from triggering. This is often easier than accepting the EventWrapper
object as the first parameter and invoking stop()
.
Returns a function of no parameters that removes any added handlers.
onevent = (jqueryObject, eventNames, match, handler) ->
throw new Error "No event handler was provided." unless handler?
wrapped = (jqueryEvent, memo) ->
Set this
to be the matched ElementWrapper, rather than the element on which the event is observed
(which is often further up the hierarchy).
elementWrapper = new ElementWrapper $(jqueryEvent.target)
eventWrapper = new EventWrapper jqueryEvent, memo
result = handler.call elementWrapper, eventWrapper, memo
If an event handler returns exactly false, then stop the event.
if result is false
eventWrapper.stop()
return
jqueryObject.on eventNames, match, wrapped
Return a function to stop listening
-> jqueryObject.off eventNames, match, wrapped
Wraps a DOM element, providing some common behaviors.
Exposes the DOM element as property element
.
class ElementWrapper
Passed the jQuery object
constructor: (query) ->
@$ = query
@element = query[0]
Some coders would use some JavaScript cleverness to automate more of the mapping from the ElementWrapper API to the jQuery API, but that eliminates a chance to write some very necessary documentation.
toString: ->
markup = @element.outerHTML
"ElementWrapper[#{markup.substring 0, (markup.indexOf ">") + 1}]"
Hides the wrapped element, setting its display to ‘none’.
hide: ->
@$.hide()
return this
Displays the wrapped element if hidden.
show: ->
@$.show()
return this
Gets or sets a CSS property. jQuery provides a lot of mapping of names to canonical names.
css: (name, value) ->
if arguments.length is 1
return @$.css name
@$.css name, value
return this
Returns the offset of the object relative to the document. The returned object has
keys top
‘ and left
‘.
offset: ->
@$.offset()
Removes the wrapped element from the DOM. It can later be re-attached.
remove: ->
@$.detach()
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.
attr: (name, value) ->
if _.isObject name
for attributeName, value of name
@attr attributeName, value
return this
current = @$.attr name
if arguments.length > 1
if value is null
@$.removeAttr name
else
@$.attr name, value
if _.isUndefined current
current = null
return current
Moves the cursor to the field.
focus: ->
@$.focus()
return this
Returns true if the element has the indicated class name, false otherwise.
hasClass: (name) ->
@$.hasClass name
Removes the class name from the element.
removeClass: (name) ->
@$.removeClass name
return this
Adds the class name to the element.
addClass: (name) ->
@$.addClass 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 an ElementWrapper, or null (to remove the body of the element).
update: (content) ->
@$.empty()
if content
@$.append (convertContent content)
return this
Appends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
append: (content) ->
@$.append (convertContent content)
return this
Prepends new content (Element, ElementWrapper, or HTML markup string) to the body of the element.
prepend: (content) ->
@$.prepend (convertContent content)
return this
Inserts new content (Element, ElementWrapper, or HTML markup string) into the DOM immediately before this ElementWrapper’s element.
insertBefore: (content) ->
@$.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) ->
@$.after (convertContent content)
return this
Finds the first child element that matches the CSS selector, wrapped as an ElementWrapper. Returns null if not found.
findFirst: (selector) ->
match = @$.find selector
if match.length
At least one element was matched, just keep the first
new ElementWrapper match.first()
else
return null
Finds all child elements matching the CSS selector, returning them as an array of ElementWrappers.
find: (selector) ->
matches = @$.find selector
for i in [0...matches.length]
new ElementWrapper matches.eq i
Find the first container element that matches the selector (wrapped as an ElementWrapper), or returns null.
findParent: (selector) ->
parents = @$.parents selector
return null unless parents.length
new ElementWrapper parents.eq(0)
Returns this ElementWrapper if it matches the selector; otherwise, returns the first container element (as an ElementWrapper) that matches the selector. Returns null if no container element matches.
closest: (selector) ->
match = @$.closest selector
switch
when match.length is 0 then return null
when match[0] is @element then return this
else return new ElementWrapper match
Returns an ElementWrapper for this element’s containing element. Returns null if this element has no parent (either because this element is the document object, or because this element is not yet attached to the DOM).
parent: ->
parent = @$.parent()
return null unless parent.length
new ElementWrapper parent
Returns an array of all the immediate child elements of this element, as ElementWrappers.
children: ->
children = @$.children()
for i in [0...children.length]
new ElementWrapper children.eq i
Returns true if this element is visible, false otherwise. This does not check to see if all containers of the element are visible.
visible: ->
@$.css("display") isnt "none"
Returns true if this element is visible, and all parent elements are also visible, up to the document body.
deepVisible: ->
element = this.element
element.offsetWidth > 0 && element.offsetHeight > 0
Fires a named event, passing an optional memo object to event handler functions. This must support common native events (exact list TBD), as well as custom events (in Prototype, custom events must have a prefix that ends with a colon).
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."
jqEvent = $.Event eventName
@$.trigger jqEvent, memo
return not jqEvent.isImmediatePropagationStopped()
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>
.
value: (newValue) ->
current = @$.val()
if arguments.length > 0
@$.val newValue
return current
Returns true if element is a checkbox and is checked
checked: ->
@element.checked
Stores or retrieves meta-data on the element. With one parameter, the current value for the name is returned (or undefined). With two parameters, the meta-data is updated and the previous value returned. For Prototype, the meta data is essentially empty (except, perhaps, for some internal keys used to store event handling information). For jQuery, the meta data may be initialized from data- attributes.
meta: (name, value) ->
current = @$.data name
if arguments.length > 1
@$.data name, value
return current
Adds an event handler for one or more events.
EventWrapper
object, and the
context (this
) is the ElementWrapper
for the matched element.Returns a function of no parameters that removes any added handlers.
on: (events, match, handler) ->
exports.on @element, events, match, handler
return this
Returns the text of the element (and its children).
text: ->
@$.text()
Wrapper around the jqXHR
object
class RequestWrapper
constructor: (@jqxhr) ->
Abort a running ajax request
abort: -> @jqxhr.abort()
Wrapper around the jqXHR
object
class ResponseWrapper
constructor: (@jqxhr, data) ->
@status = @jqxhr.status
@statusText = @jqxhr.statusText
@json = data # Mostly right? Need a content type check?
@text = @jqxhr.responseText
Retrieves a response header by name
header: (name) ->
@jqxhr.getResponseHeader name
Used to track how many active Ajax requests are currently in-process. This is incremented
when an Ajax request is started, and decremented when an Ajax request completes or fails.
The body attribute data-ajax-active
is set to the number of active Ajax requests, whenever the
count changes. This only applies to Ajax requests that are filtered through the t5/core/dom API;
other libraries (including RequireJS) which bypass this API are not counted.
activeAjaxCount = 0
adjustAjaxCount = (delta) ->
activeAjaxCount += delta
exports.body.attr "data-ajax-active", activeAjaxCount
Performs an asynchronous Ajax request, invoking callbacks when it completes.
This is very low level; most code will want to go through the t5/core/ajax
module instead,
which adds better handling of exceptions and failures, and handles Tapestry’s partial page
render reponse keys.
Error
.
Note: not really supported under jQuery, a hold-over from Prototype.
Returns the module’s exports ajaxRequest = (url, options = {}) ->
jqxhr = $.ajax
url: url
type: options.method?.toUpperCase() or "POST"
contentType: options.contentType
traditional: true
data: options.data
jQuery doesn’t have the equivalent of Protoype’s onException
error: (jqXHR, textStatus, errorThrown) ->
adjustAjaxCount -1
return if textStatus is "abort"
message = "Request to #{url} failed with status #{textStatus}"
text = jqXHR.statusText
if not _.isEmpty text
message += " -- #{text}"
message += "."
if options.failure
options.failure (new ResponseWrapper jqXHR), message
else
throw new Error message
return
success: (data, textStatus, jqXHR) ->
adjustAjaxCount -1
options.success and options.success(new ResponseWrapper jqXHR, data)
return
adjustAjaxCount +1
new RequestWrapper jqxhr
scanners = null
Sets up a scanner callback; this is used to perfom one-time setup of elements that match a particular CSS selector. The callback is passed each element that matches the selector. The callback is expected to modify the element so that it does not match future selections caused by zone updates, typically by removing the CSS class or data- attribute referenced by the selector.
scanner = (selector, callback) ->
Define a function that scans some root element (the body initially; later an updated Zone)
scan = (root) ->
callback el for el in root.find selector
return
Do it once immediately:
scan exports.body
Lazily set up a single event handler for running any added scanners.
if scanners is null
scanners = []
exports.body.on events.initializeComponents, ->
f this for f in scanners
return
scanners.push scan
return
The main export is a function that wraps a DOM element as an ElementWrapper; additional functions are attached as properties.
Returns the ElementWrapper, or null if no element with the id exists
exports = wrapElement = (element) ->
if _.isString element
element = document.getElementById 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 jQuery API (especially with respect to events).
new ElementWrapper ($ element)
Creates a new element, detached from the DOM.
createElement = (elementName, attributes, body) ->
if _.isObject elementName
body = attributes
attributes = elementName
elementName = null
if _.isString attributes
body = attributes
attributes = null
element = wrapElement document.createElement (elementName or "div")
if attributes
element.attr attributes
if body
element.update body
return element
Returns the value of a given data attribute as an object. The “data-“ prefix is added automatically. element - (object) HTML dom element attribute - (string) name of the data attribute without the “data-“ prefix.
getDataAttributeAsObject = (element, attribute) ->
value = $(element).data(attribute)
Returns the URL of a component event based on its name and an optional element or null if the event information is not found. When the element isn’t passed or it’s null, the event data is taken from the
element. getEventUrl = (eventName, element) ->
if not (eventName?)
throw 'dom.getEventUrl: the eventName parameter cannot be null'
if not _.isString eventName
throw 'dom.getEventUrl: the eventName parameter should be a string'
eventName = eventName.toLowerCase()
if element is null
element = document.body
else if element instanceof ElementWrapper
element = element.element;
else if element.jquery?
element = element[0];
Look for event data in itself first, then in the preceding siblings if not found
url = null
while not url? and element.previousElementSibling?
data = getDataAttributeAsObject(element, 'component-events')
url = data?[eventName]?.url
element = element.previousElementSibling
if not url?
Look at parent elements recursively
while not url? and element.parentElement?
data = getDataAttributeAsObject(element, 'component-events')
url = data?[eventName]?.url
element = element.parentElement;
return url;
_.extend exports,
getEventUrl: getEventUrl
wrap: wrapElement
create: createElement
ajaxRequest: ajaxRequest
Used to add an event handler to an element (possibly from elements below it in the hierarchy).
EventWrapper
object, and the context (this
)
is the ElementWrapper
for the matched elementReturns a function of no parameters that removes any added handlers.
on: (selector, events, match, handler) ->
unless handler?
handler = match
match = null
elements = $ selector
onevent elements, events, match, handler
onDocument() is used to add an event handler to the document object; this is used for global (or default) handlers. Returns a function of no parameters that removes any added handlers.
onDocument: (events, match, handler) ->
exports.on document, events, match, handler
Returns a wrapped version of the document.body element. Because all Tapestry JavaScript occurs
inside a block at the end of the document, inside the <body
> element, it is assumed that
it is always safe to get the body.
body: wrapElement document.body
scanner: scanner
return exports