- Jump To … +
  beanvalidator-validation.coffee t5-core-dom-jquery.coffee t5-core-dom-prototype.coffee ajax.coffee ajaxformloop.coffee alert.coffee autocomplete.coffee bootstrap.coffee confirm-click.coffee console.coffee datefield.coffee events.coffee exception-display.coffee exception-frame.coffee fields.coffee form-fragment.coffee forms.coffee init.coffee localdate.coffee messages.coffee moment.coffee pageinit.coffee palette.coffee select.coffee time-interval.coffee tree.coffee utils.coffee validation.coffee zone-refresh.coffee zone.coffee

- # t5-core-dom-prototype.coffee

- ¶

  Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

      http:#www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

- ¶

  ## t5/core/dom

  This is the abstraction layer that allows the majority of components to operate without caring whether the underlying infrastructure framework is Prototype, jQuery, or something else.

  The abstraction layer has a number of disadvantages:

  - It adds a number of layers of wrapper around the infrastructure framework objects
  - It is leaky; some behaviors will vary slightly based on the active infrastructure framework
  - The abstraction is alien to both Prototype and jQuery developers; it mixes some ideas from both
  - It is much less powerful or expressive than either infrastructure framework used directly

  It is quite concievable that some components will require direct access to the infrastructure framework, especially those that are wrappers around third party libraries or plugins; however many simple components may need no more than the abstract layer and gain the valuable benefit of not caring about the infrastructure framework.

      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 `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.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:

  - 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
            @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.

  - elements - array of DOM elements (or the document object)
  - eventNames - array of event names
  - match - selector to match bubbled elements, or null
  - handler - event handler function to invoke; it will be passed an `EventWrapper` instance as the first parameter, and the memo as the second parameter. `this` will be the `ElementWrapper` for the matched element.

  Event handlers may return false to stop event propagation; this prevents an event from bubbling up, and prevents any browser default behavior from triggering. This is often easier than accepting the `EventWrapper` object as the first parameter and invoking `stop()`.

  Returns a function of no parameters that removes any added handlers.

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

            wrapped = (prototypeEvent) ->

- ¶

  Set `this` to be the matched ElementWrapper, rather than the element on which the event is observed (which is often further up the hierarchy).

              elementWrapper = new ElementWrapper prototypeEvent.findElement()
              eventWrapper = new EventWrapper prototypeEvent

- ¶

  Because there’s no stopImmediatePropogation() as with jQuery, we detect if the event was stopped and simply stop calling the handler.

              result = if prototypeEvent.stopped
                        false
                      else handler.call elementWrapper, eventWrapper, eventWrapper.memo

- ¶

  If an event handler returns exactly false, then stop the event.

              if result is false
                prototypeEvent.stop()

              return

            eventHandlers = []

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

- ¶

  Return a function to remove the handler(s)

            ->
              for eventHandler in eventHandlers
                eventHandler.stop()

- ¶

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

        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 `top`‘ and `left`‘.

          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 `ElementWrapper` is returned.

  - name - the attribute to read or update, or an object of keys and values
  - value - (optional) the new value for the attribute

          attr: (name, value) ->

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

              return this

            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: ->
            element = this.element
            element.offsetWidth > 0 && element.offsetHeight > 0

- ¶

  Fires a named event, passing an optional memo object to event handler functions. This must support common native events (exact list TBD), as well as custom events (in Prototype, custom events must have a prefix that ends with a colon).

  - eventName - name of event to trigger on the wrapped Element
  - memo - optional value assocated with the event; available as WrappedeEvent.memo in event handler functions (must be null for native events). The memo, when provided, should be an object; it is an error if it is a string or other non-object type..

  Returns true if the event fully executed, or false if the event was canceled.

          trigger: (eventName, memo) ->
            throw new Error "Attempt to trigger event with null event name" unless eventName?

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

            if (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"

- ¶

  Hacky solution for TAP5-2602 (5.4 LinkSubmit does not work with Prototype JS)

            unless Prototype.Browser.WebKit and eventName == 'submit' and @element instanceof HTMLFormElement
              fireNativeEvent @element, eventName
            else
              @element.requestSubmit()

- ¶

  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

- ¶

  Returns true if element is a checkbox and is checked

          checked: ->
            @element.checked

- ¶

  Stores or retrieves meta-data on the element. With one parameter, the current value for the name is returned (or undefined). With two parameters, the meta-data is updated and the previous value returned. For Prototype, the meta data is essentially empty (except, perhaps, for some internal keys used to store event handling information). For jQuery, the meta data may be initialized from data- attributes.

  - name - name of meta-data value to store or retrieve
  - value - (optional) new value for meta-data

          meta: (name, value) ->
            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.

  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: ->
            @element.textContent or @element.innerText

- ¶

  Wrapper around the Prototype `Ajax.Request` object

        class RequestWrapper

          constructor: (@req) ->

- ¶

  Abort a running ajax request

          abort: -> throw "Cannot abort Ajax request when using Prototype."

- ¶

  Wrapper around the Prototype `Ajax.Response` object

        class ResponseWrapper

          constructor: (@res) ->

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

- ¶

  Retrieves a response header by name

          header: (name) ->
            @res.getHeader name

- ¶

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

        activeAjaxCount = 0

        adjustAjaxCount = (delta) ->
          activeAjaxCount += delta

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

- ¶

  Performs an asynchronous Ajax request, invoking callbacks when it completes.

  This is very low level; most code will want to go through the `t5/core/ajax` module instead, which adds better handling of exceptions and failures, and handles Tapestry’s partial page render reponse keys.

  - options.method - “post”, “get”, etc., default: “post”.
  - options.contentType - request content, defaults to “application/x-www-form-urlencoded”
  - options.data - optional, additional key/value pairs (for the default content type)
  - options.success - handler to invoke on success. Passed the ResponseWrapper object. Default does nothing.
  - options.failure - handler to invoke on failure (server responds with a non-2xx code). Passed the response. Default will throw the exception
  - options.exception - handler to invoke when an exception occurs (often means the server is unavailable). Passed the exception. Default will generate an exception message and throw an `Error`. Note: not really supported under jQuery, a hold-over from Prototype. Returns the module’s exports

        ajaxRequest = (url, options = {}) ->
          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.initializeComponents, ->
              f this for f in scanners
              return

          scanners.push scan

          return

- ¶

  The main export is a function that wraps a DOM element as an ElementWrapper; additional functions are attached as properties.

  - element - a DOM element, or a string id of a DOM element

  Returns the ElementWrapper, or null if no element with the id exists

        exports = wrapElement = (element) ->
          if _.isString element
            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.

  - elementName - (string) name of element to create, if ommitted, then “div”
  - attributes - (object) attributes to apply to the created element (may be omitted)
  - body - (string) content for the new element, may be omitted for no body

        createElement = (elementName, attributes, body) ->

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

          if _.isString attributes
            body = attributes
            attributes = null

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

          if attributes
            element.attr attributes

          if body
            element.update body

          return element

- ¶

  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).readAttribute('data-' + attribute)
          if value isnt null
            value = JSON.parse(value)
          else
            value = {}

- ¶

  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.

  - eventName - (string) name of the component event
  - element - (object) HTML DOM element to be used as the beginning of the event data search. Optional.

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

  - selector - CSS selector used to select elements to attach handler to; alternately, a single DOM element, or an array of DOM elements. The document is considered an element for these purposes.
  - events - one or more event names, separated by spaces
  - match - optional: CSS expression used as a filter; only events that bubble
  - up to a selected element from an originating element that matches the CSS expression will invoke the handler.
  - handler - function invoked; the function is passed an `EventWrapper` object, and the context (`this`) is the `ElementWrapper` for the matched element

  Returns a function of no parameters that removes any added handlers.

          on: (selector, events, match, handler) ->
            unless handler?
              handler = match
              match = null

            elements = parseSelectorToElements selector
            events = utils.split events
            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