base2.DOM.bind(document);
eval(base2.DOM.namespace);

var Page = {
  
  Version: 0.13,
  
  debug: false,

  request: function(url, method, parameters, target, container) {
    if (container) HTMLElement.addClass(container, 'busy');
    
    XHR.request(url, method, function(text, contentType) {
      Page.handleUpdate(text, target, contentType);
      
      if (container) HTMLElement.removeClass(container, 'busy');
    }, Page.handleError, parameters);
  },
  
  // Submit a form normally, but using a (hidden) iframe as the target in which to load the response.
  // The effect is the same as an XHR request. We use it when we wish to post data that can't be sent using XHR (like in file uploads)
  // This method is used simply by adding a 'target' attribute to your form (and including a corresponding iframe in the page)
  _formRequestViaIframe: function(form, contentTarget, container) {
    var listener, target, targets = [];
    
    if (form.target) targets = document.getElementsByName(form.target);
    if (targets.length > 0) {
      target   = targets[0];
      listener = function(event) {
        responseText = (target.contentDocument || target.contentWindow.document).body.innerHTML;
        
        //if (Item.match(Page.buildElement(responseText))) {
          Page.handleUpdate(responseText, contentTarget, 'text/html');
          HTMLElement.removeEventListener(target, 'load', listener, false);
          if (container) HTMLElement.removeClass(container, 'busy');
        //}
      };

      HTMLElement.addEventListener(target, 'load', listener, false);
      if (container) HTMLElement.addClass(container, 'busy');
      form.submit();
    }
  },
  
  linkRequest: function(link, method, target, container) {
    this.request(link.href, method || 'GET', {}, target, container);
  },
  
  formRequest: function(form, parameters, target, container) {
    if (form.target) {
      this._formRequestViaIframe(form, target, container);
    } else {
      this.request(
        HTMLElement.getAttribute(form, 'action'), 
        HTMLElement.getAttribute(form, 'method') || 'POST',
        parameters || Fields.serialize(form),
        target,
        container);
    }
  },
  
  handleError: function(text, contentType) { // todo: content types
    if (Page.debug) alert(text);
  },
  
  // Called when a successful response is received from the server.
  // We use 2 modes for updating the page: HTML, and JSON with HTML class names.
  handleUpdate: function(text, target, contentType) {
    if (contentType.indexOf('html') > -1) {
      this.updateHTML(target, text);
    } else if (contentType.indexOf('json') > -1) {
      this.updateByClass(target, eval('(' + text + ')').attributes);
    }
  },
  
  /*
    When the response is HTML, we replace, append or remove an item.
    
    The action depends on the busy container and the content being returned:
     > Any content with an ID implies a replacement
     > Empty content implies a removal
     > New item content for a collection container implies an append
  */
  updateHTML: function(container, text) {
    var element = Page.Builder.element(text);

    if (element) {
      var refElement, collection;
      
      // Replace when we have an id, or the busy container is an item
      if (element.id) {
        refElement = document.getElementById(element.id);
      }
      if (!refElement && Item.match(container)) {
        refElement = container;
      }
      
      if (refElement) {
        refElement.parentNode.replaceChild(element, refElement);
      } else {
        Page.Effects.appendAppear(element, container);
      }

      Responder.initializeTree(element);
      
    } else if (Item.match(container)) {
      Page.Effects.fadeRemove(container);
      // location redirect if deleted current page
    }
  },

  /*
    Set innerHTML by comparing object properties to class names: e.g.
      { description: 'bah', name: 'Bar' }
    will update
      <div><h1 class="name">Foo</h1><p class="description">Yup</p></div>
    to
      <div><h1 class="name">Bar</h1><p class="description">bah</p></div>
    
    This update method is used when returning a JSON for a saved record.
  */
  updateByClass: function(container, data) {
    var classNames, element, elements = container.getElementsByTagName('*');
    
    for (var i = 0; i < elements.length; i++) {
      element = elements[i];
      
      if (element.className) {
        classNames = element.className.split(/\s/);
        
        for (var j = 0; j < classNames.length; j++) {
          if (data[classNames[j]] != undefined) {
            element.innerHTML = data[classNames[j]];
          }
        }
      }
    }
  }  
};

/*--------------------------------------------------------------------------*/

var Collection = {
  
  first: function(container) {
    return this.find(container, Node.next, Node.last(container));
  },
  
  preceding: function(node) {
    return this.find(node, Node.previous);
  },
  
  // First match, start and end node inclusive
  find: function(startNode, succ, endNode) {
    var afterNode, node = startNode;
    
    if (endNode) afterNode = Node.next(endNode)
    
    while (node && node != afterNode && !this.match(node)) {
      node = succ(node);
    }
    return node;    
  },

  // Returns the associated collection container for a controling element i.e. a create link or a form.
  // This is determined by either the rel attribute, if it exists, in the case of links or the position of the control element: the collection is expected the closest preceeding collection.
  target: function(controlElement) {
    var container, containerId = controlElement.getAttribute('rel');
    
    if (!(containerId && (container = document.getElementById(containerId)))) {
      container = this.preceding(controlElement);
    }
    return container;
  },
    
  // The first input[name=ids] proceeding a collection
  idsField: function(container) {
    var node = Node.last(container);
    while (node = Node.next(node)) {
      if (node.nodeType == 1 && HTMLElement.getAttribute(node, 'name') == 'ids') {
        return node;
      }
    }
  },
  
  ids: function(container) {
    var input, ids = [];
    this.eachItem(container, function(element) {
      if (input = HTMLElement.matchSingle(element, 'input.id')) {
        ids.push( input.value );
      }
    });
    return ids;
  },
  
  // Apply the handy .first, .last and .odd classes conventionally used in styling lists
  // This is called whenever items are changed
  applyClasses: function(container) {
    var items = this.items(container);
    
    for (var i = 0; i < items.length; i++) {
      HTMLElement.removeClass(items[i], 'first');
      HTMLElement.removeClass(items[i], 'last');
      HTMLElement.removeClass(items[i], 'odd');

      if (i == 0) {
        HTMLElement.addClass(items[i], 'first');
      }
      if (i == items.length - 1) {
        HTMLElement.addClass(items[i], 'last');
      }
      if (i % 2 == 0) {
        HTMLElement.addClass(items[i], 'odd');
      }
    }
  },
  
  items: function(container) {
    var elements = [];
    this.eachItem(container, function(element) {
      elements.push(element);
    });
    return elements;
  },
  
  eachItem: function(container, callback) {
    for (var i = 0; i < container.childNodes.length; i++) {
      if (Node.element(container.childNodes[i]) && Item.match(container.childNodes[i])) {
        callback(container.childNodes[i]);
      }
    }
  },
  
  match: function(node) {
    if (node.nodeType == 1) {
      var tagName = node.tagName.toLowerCase();
      return tagName == 'ul' || tagName == 'tbody' || HTMLElement.hasClass(node, 'collection');      
    }
  }
};


var Item = {
  
  match: function(element) {
    var tagName = element.tagName.toLowerCase();
    return tagName == 'li' || tagName == 'tr' || HTMLElement.hasClass(element, 'item');
  },
  
  find: function(node, prop) {
    do {
      node = node[prop];
    } while (node && !(node.nodeType == 1 && Item.match(node)));
    return node;
  },
        
  containing: function(element) { return Item.find(element, 'parentNode') },
  
  previous: function(element) {
    return Item.find(element, 'previousSibling');
  },
  
  next: function(element) {
    return Item.find(element, 'nextSibling');
  },
        
  forward: function(element) {
    var next = Item.next(element);
    if (next) {
      next.parentNode.insertBefore(element, next.nextSibling);
      Collection.applyClasses(element.parentNode);
    }
  },
  
  back: function(element) {
    var previous = Item.previous(element);
    if (previous) {
      previous.parentNode.insertBefore(element, previous);
      Collection.applyClasses(element.parentNode);
    }
  }
};

/*--------------------------------------------------------------------------*/

// Interface to the crazy XHR object
var XHR = {
  
  defaultHeaders: {
    'X-Requested-With':  'XMLHttpRequest', // keep compatibility with Ajax in Rails
    'Content-type':      'application/x-www-form-urlencoded',
    'Accept':            'text/html, application/json, text/xml, */*',
    'If-Modified-Since': 'Thu, 1 Jan 1970 00:00:00 GMT' // Stop IE7 caching
  },
  
  request: function(url, method, onSuccess, onError, parameters, headers) {
    headers = headers || {};
    parameters = parameters || {};
    
    method = method.toUpperCase();
    if (method != 'GET' && method != 'POST') {
      parameters._method = method;
      method = 'POST';
    }
    
    var queryString, components = [];
    for (prop in parameters) {
      components.push(encodeURIComponent(prop) + '=' + encodeURIComponent(parameters[prop]));
    }
    queryString = components.join('&');
    
    if (method == 'GET') {
      url += '?' + queryString;
      queryString = null;
    }
    
    var callback, object = this.newObject();
    
    object.open(method, url, true);
    object.onreadystatechange = function() {
      if (object.readyState == 4) {
        if (object.status == 500) {
          callback = onError;
        } else if (object.status >= 200 && object.status < 300) {
          callback = onSuccess;
        }

        callback(object.responseText, object.getResponseHeader('Content-Type'));
        object.onreadystatechange = function() {};
      }
    }
    
    for (var prop in XHR.defaultHeaders) {
      object.setRequestHeader(prop, XHR.defaultHeaders[prop]);
    }
    for (var prop in headers) {
      object.setRequestHeader(prop, headers[prop]);
    }
    
    object.send(queryString);
  },
  
  newObject: function() {
    try {
      return new ActiveXObject('Msxml2.XMLHTTP')
    } catch(error) {
      try {
        return new ActiveXObject('Microsoft.XMLHTTP')
      } catch(error) {
        return new XMLHttpRequest()
      }
    }
  }
};

/*--------------------------------------------------------------------------*/

// Todo: move this to Responders - 'new' / 'removed'
Page.Effects = {
  fadeRemove: function(element) {
    var collectionContainer = element.parentNode;
    
    new Fx.Opacity(element, { duration: 350, onComplete: function() {
      collectionContainer.removeChild(element);
      Collection.applyClasses(collectionContainer);
    } }).custom(1, 0);
  },
  
  appendAppear: function(element, container) {
    var effect  = new Fx.Opacity(element, { duration: 350 });
    
    effect.setStyle(element, 'opacity', 0);
    container.appendChild(element);
    Collection.applyClasses(container);
    effect.custom(0, 1);
  }
};

/*--------------------------------------------------------------------------*/

// Uses innerHTML to create DOM nodes in the current document from HTML text.
// New HTML should only come in server responses, never coded in your JS script!
Page.Builder = {
  
  // Parses the first tag in text, and tries to build a DOM node
  element: function(text) {
    var element, containingTags, tagName, matches = text.match(/<(\w+)/);

    if (matches) {
      element        = document.createElement('div');
      text           = text.replace(/^[^<]+/, ''); // ensure the element is firstChild
      containingTags = this._containingTags(tagName);
      tagName        = matches[1].toLowerCase();
      
      // Wrap the element in it's required containers
      for (var i = 0; i < containingTags.length; i++) text = "<" + containingTags[i] + ">" + text + "</" + containingTags[i] + ">";
      element.innerHTML = text;
      for (var i = 0; i < containingTags.length + 1; i++) element = element.firstChild;
    }
    return element;
  },
  
  _containingTags: function(name) {
    var containingTags = [], containingTag = this._basicContainerMap[name];
    if (containingTag) {
      containingTags = [containingTag].concat(this._containingTags(containingTag));
    }
    return containingTags;
  },
  
  // Container tag names for elements that cannot be created outside of a certain containing element type
  _basicContainerMap: {
    li:    'ul',
    td:    'tr',
    tr:    'tbody',
    tbody: 'table'
  }
}

/*--------------------------------------------------------------------------*/
// Handy helpers for DOM traversal

Node = {
  element: function(node, tagName) {
    if (node 
      && node.nodeType == 1 
      && (!tagName || node.tagName.toLowerCase() == tagName.toLowerCase())) {
        return node;
      }
  },
  
  next: function(node) {
    var _node = node.firstChild;
    while (node && !_node) {
      _node = node.nextSibling;
      node = node.parentNode;
    }
    return _node;
  },
  
  find: function(node, prop, test) {
    do {
      node = node[prop];
    } while (node && !test(node));
    return node;
  },
  
  previous: function(node) {
    return node.previousSibling ? Node.last(node.previousSibling) : node.parentNode;
  },
  
  last: function(node) {
    while (node.lastChild) node = node.lastChild;
    return node;
  }
};

/*  
  Responder, unobtrusive JavaScript framework
  Copyright 2007 Adam Bones
  
  See http://www.boxpop.net/responder
  
  This code is freely distributable under the terms of an MIT-style license.
/*--------------------------------------------------------------------------*/

var Responder = {
  
  Version:  0.3,
  Classes:  {},
  Listener: /^on(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mousedown|mousemove|mouseout|mouseover|mouseup|reset|resize|select|submit|unload)(\w+)$/i,
  
  debug: false,

  create: function(className, source) {
    var klass, matches, listeners = [];

    for (var prop in source) {
      matches = prop.match(this.Listener);
      if (matches) {
        listeners.push([matches[1].toLowerCase(), matches[2].charAt(0).toLowerCase() + matches[2].substring(1), prop]);
      }
    }

    klass = function() {
      if (this.initialize) {
        this.initialize.apply(this, arguments);
      }
      for (var i = 0; i < listeners.length; i++) {
        var type = listeners[i][0], object = this[listeners[i][1]], method = listeners[i][2];
        
        if (object && object.nodeType && object.nodeType > 0) {
          HTMLElement.addEventListener(object, type, Responder._createListener(this, method, className), false);
        }
      }
    }

    for (var prop in Responder.Methods) {
      klass.prototype[prop] = Responder.Methods[prop];
    }
    for (var prop in source) {
      klass.prototype[prop] = source[prop];
    }
    
    Responder.Classes[className] = Responder.Classes[className] || [];
    Responder.Classes[className].push(klass);
    
    return klass;
  },
  
  initializeDocument: function() {
    Responder.initializeTree(document);
  },
  
  initializeTree: function(node, excludeRoot) {
    if (node.getElementsByTagName) {
      if (!excludeRoot && node.className) {
        Responder.initializeForElement(node);
      }
      
      for (var elements = node.getElementsByTagName('*'), i = 0; i < elements.length; i++) {
        Responder.initializeForElement(elements[i]);
      }
    } else if (node.childNodes) {
      for (var i = 0; i < node.childNodes.length; i++) {
        Responder.initializeTree(node.childNodes[i]);
      }      
    }
    return node;
  },
  
  initializeForElement: function(container) {
    var classNames = container.className.split(/\s+/);
    for (var i = 0; i < classNames.length; i++) {
      this.initializeForClass(classNames[i], container);
    }
  },
  
  initializeForClass: function(name, container) {
    var classes = Responder.Classes[name];
    if (classes) {
      for (var i = 0; i < classes.length; i++) {
        new classes[i](container);
      }
    }
  },
  
  _createListener: function(instance, method, instanceName) {
    var listener = function(event) {
      instance[method](event);
    };
    if (this.debug) {
      return function(event) {
        try {
          listener(event);
        } catch(error) {
          alert('Error in ' + instanceName + '#' + method + ': ' + (error.message || error));
        }
      }
    } else {
      return listener;
    }
  },
  
  _createAssigners: function() {
    for (var i = 0; i < arguments.length; i++) {
      var name   = arguments[i];
      var method = 'assignBy' + name.charAt(0).toUpperCase() + name.substring(1);
      var deprecatedMethod = '_' + method;
      
      this.Methods[method] = this.Methods[deprecatedMethod] = this._createAssigner(name);
    }
  },
  
  _createAssigner: function(attributeName) {
    return function(container) {
      var values, assigned = [], elements = container.getElementsByTagName('*');
      
      if (arguments.length == 2) {
        values = arguments[1].split(/\s+/);
      } else {
        values = [];
        for (var i = 1; i < arguments.length; i++) {
          values.push(arguments[i]);
        }
      }

      for (var i = 0; i < elements.length; i++) {
        var element = elements[i], value = HTMLElement.getAttribute(element, attributeName) || element[attributeName]; // getAttribute failing in IE for some attribute names

        if (value) { 
          for (var j = 0; j < values.length; j++) {
            if (!this[values[j]] && value.match(new RegExp("(^|\\s)" + values[j] + "(\\s|$)"))) {
          		this[values[j]] = element;
          		assigned.push(values[j]);
            }
          }        
        }
      }
      return assigned;
    };
  }
};

Responder.Methods = {
  
  addClass: function(element, name) {
    HTMLElement.addClass(element, name);
    return this[name] = element;
  },
  
  removeClass: function(element, name) {
    HTMLElement.removeClass(element, name);
    return this[name] = null;
  },
  
  assignContainerByClass: function(node, className) {
    var classNames = className.split(/\s/);
    
    while (node) {
      if (node.className) {        
        for (var j = 0; j < classNames.length; j++) {
          if (HTMLElement.hasClass(node, classNames[j])) {
            return this[classNames[j]] = node;
          }
        }
      }
      node = node.parentNode;
    }
  }
};

Responder._createAssigners('class', 'name', 'type');

document.addEventListener('DOMContentLoaded', Responder.initializeDocument, false);