t5-core-dom-prototype.coffee | |
---|---|
Copyright 2012-2013 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
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/domThis 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) -> |
Save a local reference to Prototype.$ ... see notes about some challenges using Prototype, jQuery, and RequireJS together, here: https://github.com/jrburke/requirejs/issues/534 | $ = window.$ |
Fires a native event; something that Prototype does not normally do. Returns true if the event completed normally, false if it was canceled. | fireNativeEvent = (element, eventName) ->
if document.createEventObject |
IE support: | event = document.createEventObject()
return element.fireEvent "on#{eventName}", event |
Everyone else: | event = document.createEvent "HTMLEvents"
event.initEvent eventName, true, true
element.dispatchEvent event
return not event.defaultPrevented |
converts a selector to an array of DOM elements | parseSelectorToElements = (selector) ->
if _.isString selector
return $$ selector |
Array is assumed to be array of DOM elements | if _.isArray selector
return selector |
Assume its a single DOM element | [selector] |
Converts content (provided to | convertContent = (content) ->
if _.isString content
return content
if _.isElement content
return content
if content instanceof ElementWrapper
return content.element
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:
| class EventWrapper
constructor: (event) ->
@nativeEvent = event
@memo = event.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: -> |
There's no equivalent to stopImmediatePropagation() unfortunately. | @nativeEvent.stop() |
Interface between the dom's event model, and Prototype's.
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 | onevent = (elements, eventNames, match, handler) ->
throw new Error "No event handler was provided." unless handler?
wrapped = (prototypeEvent) -> |
Set | elementWrapper = new ElementWrapper prototypeEvent.findElement()
eventWrapper = new EventWrapper prototypeEvent |
Because there's no stopImmediatePropogation() as with jQuery, we detect if the event was stopped and simply stop calling the handler. | result = if prototypeEvent.stopped
false
else handler.call elementWrapper, eventWrapper, eventWrapper.memo |
If an event handler returns exactly false, then stop the event. | if result is false
prototypeEvent.stop()
return
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 DOM element as property | class ElementWrapper
constructor: (@element) -> |
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: ->
@element.hide()
return this |
Displays the wrapped element if hidden. | show: ->
@element.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 @element.getStyle name
@element.setStyle name: value
return this |
Returns the offset of the object relative to the document. The returned object has
keys | offset: ->
@element.viewportOffset() |
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
| attr: (name, value) ->
if _.isObject name
for attributeName, value of name
@attr attributeName, value
return this
current = @element.readAttribute name
if arguments.length > 1 |
Treat undefined and null the same; Prototype does something slightly odd, treating undefined as a special case where the attribute value matches the attribute name. | @element.writeAttribute name, if value is undefined then null else value
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 an ElementWrapper, or null (to remove the body of the element). | 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 |
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
new ElementWrapper(e) for e in matches |
Find the first container element that matches the selector (wrapped as an ElementWrapper), or returns null. | findParent: (selector) ->
parent = @element.up selector
return null unless parent
new ElementWrapper parent |
Returns this ElementWrapper if it matches the selector; otherwise, returns the first container element (as an ElementWrapper) that matches the selector. Returns null if no container element matches. | closest: (selector) ->
if @element.match selector
return this
return @findParent selector |
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 = @element.parentNode
return null unless parent
new ElementWrapper parent |
Returns an array of all the immediate child elements of this element, as ElementWrappers. | children: ->
new ElementWrapper(e) for e in @element.childElements() |
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. | deepVisible: ->
cursor = this
while cursor
return false unless cursor.visible()
cursor = cursor.parent()
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).
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
| value: (newValue) ->
current = @element.getValue()
if arguments.length > 0
@element.setValue 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 = @element.retrieve name
if arguments.length > 1
@element.store name, value
return current |
Adds an event handler for one or more events.
| on: (events, match, handler) ->
exports.on @element, events, match, handler
return this |
Returns the text of the element (and its children). | text: ->
@element.textContent or @element.innerText |
Wrapper around the Prototype | class RequestWrapper
constructor: (@req) -> |
Abort a running ajax request | abort: -> throw "Cannot abort Ajax request when using Prototype." |
Wrapper around the Prototype | class ResponseWrapper
constructor: (@res) ->
@status = res.status
@statusText = res.statusText
@json = res.responseJSON
@text = res.responseText |
Retrieves a response header by name | header: (name) ->
@res.getHeader name |
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 | activeAjaxCount = 0
adjustAjaxCount = (delta) ->
activeAjaxCount += delta
exports.body.attr "data-ajax-active", (activeAjaxCount > 0) |
Performs an asynchronous Ajax request, invoking callbacks when it completes. This is very low level; most code will want to go through the
| ajaxRequest = (url, options = {}) ->
finalOptions =
method: options.method or "post"
contentType: options.contentType or "application/x-www-form-urlencoded"
parameters: options.data
onException: (ajaxRequest, exception) ->
adjustAjaxCount -1
if options.exception
options.exception exception
else
throw exception
return
onFailure: (response) ->
adjustAjaxCount -1
message = "Request to #{url} failed with status #{response.getStatus()}"
text = response.getStatusText()
if not _.isEmpty text
message += " -- #{text}"
message += "."
if options.failure
options.failure (new ResponseWrapper response), message
else
throw new Error message
return
onSuccess: (response) ->
adjustAjaxCount -1 |
Prototype treats status == 0 as success, even though it may indicate that the server didn't respond. | if (not response.getStatus()) or (not response.request.success())
finalOptions.onFailure(new ResponseWrapper response)
return |
Tapestry 5.3 includes lots more exception catching ... that just got in the way of identifying the source of problems. That's been stripped out. | options.success and options.success(new ResponseWrapper response)
return
adjustAjaxCount +1
new RequestWrapper (new Ajax.Request url, finalOptions)
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.zone.didUpdate, ->
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 = $ 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 |
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
_.extend exports,
wrap: wrapElement
create: createElement
ajaxRequest: ajaxRequest |
Used to add an event handler to an element (possibly from elements below it in the hierarch).
| on: (selector, events, match, handler) ->
unless handler?
handler = match
match = null
elements = parseSelectorToElements selector
events = utils.split events
onevent elements, 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: wrapElement document.body
scanner: scanner
return exports
|