       parent.removeChild scriptElement
       parent.insertBefore scriptElement.cloneNode(true), nextSibling
     null
+  
+  # Do AJAX.
+  #
+  # @param {String} url the request URL (e.g., "http://localhost/path/to.html")
+  # @param {String} method the request method (e.g., "POST")
+  # @param [HTMLFormElement] form the DOM form whose data will be submitted
+  # @param [function(data)] onData callback that receives the XHR data, if the
+  #                                XHR completes successfully
+  xhr: (url, method, form, onData) ->
+    xhr = new XMLHttpRequest
+    xhr.onload = @_xhr_onload
+    xhr.pwnfxOnData = onData
+    xhr.open xhrMethod, xhrUrl
+    xhr.send new FormData(xhrForm)    
+  
+  # Called when an XHR request issued by PwnFx.xhr works out.
+  _xhr_onload: ->
+    if @status < 200 || @status >= 300
+      throw new Error("XHR result ignored due to HTTP status: #{@statusText}")
+    @pwnfxOnData @responseText
   
 # Singleton instance.
 PwnFx = new PwnFxClass

  
  # Performs a scoped querySelectAll.
  #
  # @param [Element] scope the DOM element serving as the search scope
  # @param [String] selector the CSS selector to query
  # @return [NodeList, Array] the elements in the scope that match the CSS
  #     selector; the scope container can belong to the returned array
  queryScope: (scope, selector) ->
    scopeMatches = false

  
  # Registers a PwnFx effect.
  #
  # @param {String} attrName string following data-pwnfx- in the effect's
  #                          attribute names
  # @param klass the class that wraps the effect's implementation
  registerEffect: (attrPrefix, klass) ->
