Index: popups.js
===================================================================
--- popups.js	(revision 13)
+++ popups.js	(working copy)
@@ -1,4 +1,4 @@
-// $Id: popups.js,v 1.9.2.27 2009/03/11 22:17:43 starbow Exp $
+// $Id: popups.js,v 1.9.8.17 2009/11/11 16:45:20 longwave Exp $
 
 /**
  * Popup Modal Dialog API
@@ -8,14 +8,15 @@
  *
  */
 
-/**
- * Create the popups object/namespace.
+/*
+ * TODO
+ * * Return key in add node form not working.
+ * * Tabledrag breaking after ahah reload.
  */
-Drupal.popups = function() {};
 
-function isset(v) {
-  return (typeof(v) !== 'undefined');
-}
+// ***************************************************************************
+// DRUPAL Namespace
+// ***************************************************************************
 
 /**
  * Attach the popups bevior to the all the requested links on the page.
@@ -23,154 +24,510 @@
  * @param context
  *   The jQuery object to apply the behaviors to.
  */
+
 Drupal.behaviors.popups = function(context) {
+  Popups.saveSettings();
+  
   var $body = $('body');
   if(!$body.hasClass('popups-processed')) {
     $body.addClass('popups-processed');
-    $(document).bind('keydown', Drupal.popups.keyHandle);
-    $popit = $('#popit');
+    $(document).bind('keydown', Popups.keyHandle);
+    var $popit = $('#popit');
     if ($popit.length) {
       $popit.remove();
-      Drupal.popups.message($popit.html());
+      Popups.message($popit.html());
     }
   }
   
   // Add the popups-link-in-dialog behavior to links defined in Drupal.settings.popups.links array.
-  if (Drupal.settings.popups.links) {
+  // Get these from current Drupal.settings, not Popups.originalSettings, as each page has it's own hooks.
+  if (Drupal.settings.popups && Drupal.settings.popups.links) {
     jQuery.each(Drupal.settings.popups.links, function (link, options) { 
-      if (isset(options.noReload)) { // Using obsolete name.
-        options.noUpdate = options.noReload; // Don't break existing sites with name change.
-      }
-      Drupal.popups.attach(context, link, options);
+      Popups.attach(context, link, Popups.options(options));
     });
   }
   
-  Drupal.popups.attach(context, '.popups', {noUpdate: true});  
-  Drupal.popups.attach(context, '.popups-form', {}); // ajax reload.
-  Drupal.popups.attach(context, '.popups-form-reload', {reloadWhenDone: true}); // whole page reload. 
-  Drupal.popups.attach(context, '.popups-form-noupdate', {noUpdate: true});  // no reload at all.
-  Drupal.popups.attach(context, '.popups-form-noreload', {noUpdate: true});  // Obsolete.
+  Popups.attach(context, '.popups', Popups.options({updateMethod: 'none'}));  
+  Popups.attach(context, '.popups-form', Popups.options({updateMethod: 'ajax'})); // ajax reload.
+  Popups.attach(context, '.popups-form-reload', Popups.options({updateMethod: 'reload'})); // whole page reload. 
+  Popups.attach(context, '.popups-form-noupdate', Popups.options({updateMethod: 'none'}));  // no reload at all.
+};
+
+// ***************************************************************************
+// Popups Namespace **********************************************************
+// ***************************************************************************
+/**
+ * The Popups namespace contains:
+ * * An ordered stack of Popup objects,
+ * * The state of the original page,
+ * * Functions for managing both of the above.
+ */
+Popups = function(){};
+
+/**
+ * Static variables in the Popups namespace.
+ */
+Popups.popupStack = []; 
+Popups.addedCSS = [];
+Popups.addedJS = [];
+Popups.originalSettings = null; // The initial popup options of the page.
+/**
+ * Each popup object gets it's own set of options.
+ * These are the defaults.
+ */
+Popups.defaultOptions = {
+  doneTest: null, // null, *path*, *regexp*. how do we know when a multiform flow is done?
+  updateMethod: 'ajax', // none, ajax, reload, *callback*
+  updateSource: 'initial', // initial, final. Only used if updateMethod != none.
+  onUpdate: '', // Only used if updateMethod == callback.
+  href: null, 
+  width: null, // Override the width specified in the css.
+  targetSelectors: null, // Hash of jQuery selectors that define the content to be swapped out.
+  titleSelectors: null, // Array of jQuery selectors to place the new page title.
+  reloadOnError: false, // Force the entire page to reload if the popup href is unaccessable.
+  noMessage: false, // Don't show drupal_set_message messages.   
+  skipDirtyCheck: false, // If true, this popup will not check for edits on the originating page.  
+  hijackDestination: true // Use the destiination param to force a form submit to return to the originating page. 
+};
+
+// ***************************************************************************
+// Popups.Popup Object *******************************************************
+// ***************************************************************************
+/**
+ * A Popup is a single modal dialog.
+ * The popup object encapslated all the info about a single popup.
+ */
+Popups.Popup = function() {
+  this.id = 'popups-' + Popups.nextCounter();
   
+  // These properties are needed if the popup contains a form that will be ajax submitted.
+  this.parent = null; // The popup that spawned this one. If parent is null, this popup was spawned by the original page.
+  this.path = null; // If popup is showing content from a url, this is that path.
+  this.element = null; // The DOM element that was clicked to launch this popup.
+  this.options = null; // An option array that control how the popup behaves.  See Popups.defaultOptions for explainations.
 };
+Popups.Popup.prototype.$popup = function() {
+  return $('#' + this.id);
+};
+Popups.Popup.prototype.$popupBody = function() {
+  return $('#' + this.id + ' .popups-body');
+};
+Popups.Popup.prototype.$popupClose = function() {
+  return $('#' + this.id + ' .popups-close');
+};
+Popups.Popup.prototype.$popupTitle = function() {
+  return $('#' + this.id + ' .popups-title');
+};
+Popups.Popup.prototype.$popupButtons = function() {
+  return $('#' + this.id + ' .popups-buttons');
+};
+Popups.Popup.prototype.$popupFooter = function() {
+  return $('#' + this.id + ' .popups-footer');
+};
 
 /**
- * Attach the popups behavior to a particular link.
+ * Create the jQuery wrapped html at the heart of the popup object.
+ * 
+ * @param title
+ *   String
+ * @param body
+ *   String/HTML
+ * @param buttons
+ *   Hash/Object
+ * @return
+ *   The $popup.
+ */
+Popups.Popup.prototype.fill = function(title, body, buttons) {
+  return $(Drupal.theme('popupDialog', this.id, title, body, buttons));
+}
+
+/**
+ * Hide the popup by pushing it off to the side. 
+ * Just making it display:none causes flash in FF2.
+ */
+Popups.Popup.prototype.hide = function() {
+  this.$popup().css('left', '-9999px');
+};
+
+Popups.Popup.prototype.show = function() {
+  Popups.resizeAndCenter(this);
+};
+
+Popups.Popup.prototype.open = function(title, body, buttons, width){
+  return Popups.open(this, title, body, buttons, width);
+};
+
+Popups.Popup.prototype.removePopup = function() { 
+  Popups.removePopup(this);
+}; 
+
+/**
+ * Remove everything.
+ */
+Popups.Popup.prototype.close = function() {
+  return Popups.close(this);
+};
+
+/**
+ * Set the focus on the popups to the first visible, enabled form element, or the close link.
+ */
+Popups.Popup.prototype.refocus = function() {
+  // Select the first visible enabled input element.
+  var $popup = this.$popup();
+  var $focus = $popup.find(':input:visible:enabled:first');
+  if (!$focus.length) {
+    // There is no visible enabled input element, so select the close link.
+    $focus = $popup.find('.popups-close a');
+  }
+  $focus.focus();
+};
+
+/**
+ * Return a selector that will find target content on the layer that spawned this popup.
+ * This is needed for the popup to do ajax updates. 
+ */
+Popups.Popup.prototype.targetLayerSelector = function() {
+  if (this.parent === null) {
+    return 'body'; // Select content in the original page.
+  }
+  else {
+    return '#' + this.parent.id; // Select content in the parent popup.
+  } 
+};
+
+/**
+ * Determine if we are at an end point of a form flow, or just moving from one popups to another.
+ * 
+ * @param path
+ *   The path of the page that the form flow has moved to.
+ *   This path is relative to the base_path.  
+ *   Ex: node/add/story, not http://localhost/drupal6/node/add/story or drupa6/node/add/story.
+ * @return bool
+ */ 
+Popups.Popup.prototype.isDone = function(path) {
+  var done;
+  if (this.options.doneTest) {
+    // Test if we are at the path specified by doneTest.
+    done = (path === this.options.doneTest || path.match(this.options.doneTest));    
+  }
+  else { 
+    if (this.parent) {
+       // Test if we are back to the parent popup's path.
+      done = (path === this.parent.path);     
+    }
+    else {
+       // Test if we are back to the original page's path.
+      done = (path === Popups.originalSettings.popups.originalPath);
+    }
+  }; 
+  return done;  
+};
+
+
+// ***************************************************************************
+// Popups Functions **********************************************************
+// ***************************************************************************
+
+/**
+ * Test if the param has been set. 
+ * Used to distinguish between a value set to null or false and on not yet unset.
+ */
+Popups.isset = function(v) {
+  return (typeof(v) !== 'undefined');
+};
+
+/**
+ * Get the currently active popup in the page.
+ * Currently it is the only one visible, but that could change.
+ */
+Popups.activePopup = function() {
+  if (Popups.popupStack.length) {
+    return Popups.popupStack[Popups.popupStack.length - 1]; // top of stack.
+  }
+  else {
+    return null;
+  }
+};
+
+/**
+ * Manage the page wide popupStack.
+ */
+Popups.push = function(popup) {
+  Popups.popupStack.push(popup);
+};
+// Should I integrate this with popupRemove??
+Popups.pop = function(popup) {
+  return Popups.popupStack.pop();
+};
+
+/**
+ * Build an options hash from defaults.
+ * 
+ * @param overrides
+ *   Hash of values to override the defaults.
+ */
+Popups.options = function(overrides) {
+  var defaults = Popups.defaultOptions;
+  return Popups.overrideOptions(defaults, overrides);  
+}
+
+/**
+ * Build an options hash.  
+ * Also maps deprecated options to current options.
+ * 
+ * @param defaults
+ *   Hash of default values
+ * @param overrides
+ *   Hash of values to override the defaults with.
+ */
+Popups.overrideOptions = function(defaults, overrides) {
+  var options = {};
+  for(var option in defaults) {
+    var value;
+    if (Popups.isset(overrides[option])) {
+      options[option] = overrides[option];
+    }
+    else {     
+      options[option] = defaults[option];
+    }
+  }
+  // Map deprecated options.
+  if (overrides['noReload'] || overrides['noUpdate']) {
+    options['updateMethod'] = 'none';
+  } 
+  if (overrides['reloadWhenDone']) {
+    options['updateMethod'] = 'reload';
+  } 
+  if (overrides['afterSubmit']) {
+    options['updateMethod'] = 'callback';
+    options['onUpdate'] = overrides['afterSubmit'];
+  } 
+  if (overrides['forceReturn']) {
+    options['doneTest'] = overrides['forceReturn'];
+  } 
+  return options;
+}
+
+/**
+ * Attach the popups behavior to all elements inside the context that match the selector.
  *
+ * @param context
+ *   Chunk of html to search.
  * @param selector
- *   jQuery selector for links to attach popups behavior to.
+ *   jQuery selector for elements to attach popups behavior to.
  * @param options
  *   Hash of options associated with these links.
  */
-Drupal.popups.attach = function(context, selector, options) {
+Popups.attach = function(context, selector, options) {
   $(selector, context).not('.popups-processed').each(function() {
-    var $element = $(this);
-    // Mark the element as attached.    
-    var title = $element.attr('title') || '';
-    $element.attr('title', title + Drupal.t('[Popup]')); // Append note to link title.
+    var $element = $(this);  
+    
+    // Mark the element as processed.    
     $element.addClass('popups-processed');
     
+    // Append note to link title.
+    var title = '';
+    if ($element.attr('title')) {
+      title = $element.attr('title') + ' ';
+    }
+    title += Drupal.t('[Popup]');
+    $element.attr('title', title); 
+    
     // Attach the on-click popup behavior to the element.
-    $element.click(function(e){ 
-      var element = this;
+    $element.click(function(event){
+      return Popups.clickPopupElement(this, options);
+    });
+  });
+};    
 
-      // If element is inside of a #popup div, show alert and bail out. 
-      if ($(element).parents('#popups').length) { 
-        alert("Sorry, popup chaining is not supported (yet).");
-        return false;
-      }
+/**
+ * Respond to click by opening a popup.
+ * 
+ * @param element
+ *   The element that was clicked.
+ * @param options
+ *   Hash of options associated with the element.
+ */
+Popups.clickPopupElement = function(element, options) {
+  Popups.saveSettings();
+  
+  // If the element contains a on-popups-options attribute, override default options param.
+  if ($(element).attr('on-popups-options')) {
+    var overrides = Drupal.parseJson($(element).attr('on-popups-options')); 
+    options = Popups.overrideOptions(options, overrides);
+  }
+	
+  // The parent of the new popup is the currently active popup.
+  var parent = Popups.activePopup();
+  
+  // If the option is distructive, check if the page is already modified, and offer to save.
+  var willModifyOriginal = !(options.updateMethod === 'none' || options.skipDirtyCheck);
+  if (willModifyOriginal && Popups.activeLayerIsEdited()) {
+    // The user will lose modifications, so show dialog offering to save current state.
+    Popups.offerToSave(element, options, parent);
+  }
+  else {
+    // Page is clean, or popup is safe, so just open it.
+    Popups.openPath(element, options, parent);
+  }
+  return false; 
+};
 
-      // If the element contains a on-popups-options attribute, use it instead of options param.
-      if ($(element).attr('on-popups-options')) {
-        options = eval('(' + $(element).attr('on-popups-options') + ')'); 
-      }
+/**
+ * Test if the active layer been edited.
+ * Active layer is either the original page, or the active Popup.
+ */
+Popups.activeLayerIsEdited = function() {
+  var layer = Popups.activePopup();
+  var $context = Popups.getLayerContext(layer);
+  // TODO: better test for edited page, maybe capture change event on :inputs.   
+  var edited = $context.find('span.tabledrag-changed').length;  
+  return edited;
+}
 
-      // If the option is distructive, check if the page is already modified, and offer to save.
-      var pageIsDirty = $('span.tabledrag-changed').size() > 0;
-      var willModifyOriginal = !options.noUpdate;
-      if (pageIsDirty && willModifyOriginal) {
-        // The user will lose modifications, so popups dialog offering to save current state.
-        var body = Drupal.t("There are unsaved changes on this page, which you will lose if you continue.");
-        var buttons = {
-         'popup_save': {title: Drupal.t('Save Changes'), func: function(){Drupal.popups.savePage(element, options);}},
-         'popup_submit': {title: Drupal.t('Continue'), func: function(){Drupal.popups.removePopup(); Drupal.popups.openPath(element, options);}},
-         'popup_cancel': {title: Drupal.t('Cancel'), func: Drupal.popups.close}
-        };
-        return Drupal.popups.open( Drupal.t('Warning: Please Confirm'), body, buttons );
-      }
-      else {
-        return Drupal.popups.openPath(element, options);
-      } 
-    });    
-  });
+/**
+ * Show dialog offering to save form on parent layer.
+ * 
+ * @param element
+ *   The DOM element that was clicked.
+ * @param options
+ *   The options associated with that element.
+ * @param parent
+ *   The layer that has the unsaved edits.  Null means the underlying page.
+ */
+Popups.offerToSave = function(element, options, parent) {
+  var popup = new Popups.Popup();
+  var body = Drupal.t("There are unsaved changes in the form, which you will lose if you continue.");
+  var buttons = {
+   'popup_save': {title: Drupal.t('Save Changes'), func: function(){Popups.saveFormOnLayer(element, options, parent);}},
+   'popup_submit': {title: Drupal.t('Continue'), func: function(){popup.removePopup(); Popups.openPath(element, options, parent);}},
+   'popup_cancel': {title: Drupal.t('Cancel'), func: function(){popup.close();}}
+  };
+  popup.open(Drupal.t('Warning: Please Confirm'), body, buttons);  
 };
 
-
 /**
  * Generic dialog builder.
+ * Adds the newly built popup into the DOM.
+ * 
+ * TODO: capture the focus if it tabs out of the dialog.
+ *
+ * @param popup
+ *   Popups.Popup object to fill with content, place in the DOM, and show on the screen.
+ * @param String title
+ *   String: title of new dialog.
+ * @param body (optional)
+ *   String: body of new dialog.
+ * @param buttons (optional)
+ *   Hash of button parameters.
+ * @param width (optional)
+ *   Width of new dialog.
+ *   
+ * @return popup object
  */
-Drupal.popups.open = function(title, body, buttons, width) {
-  Drupal.popups.addOverlay(); // TODO - nonModal option.
-  var $popups = $(Drupal.theme('popupDialog', title, body, buttons));
-  // Start with dialog off the side. Making it invisible causes flash in FF2.
-  $popups.css('left', '-9999px');
+Popups.open = function(popup, title, body, buttons, width){
+  Popups.addOverlay();
+  
+  if (Popups.activePopup()) {
+    // Hiding previously active popup.
+    Popups.activePopup().hide();
+  }
+  
+  if (!popup) {
+    // Popup object was not handed in, so create a new one.
+    popup = new Popups.Popup();
+  }
+  Popups.push(popup); // Put this popup at the top of the stack.
+
+  // Create the jQuery wrapped html for the new popup.
+  var $popup = popup.fill(title, body, buttons);
+  popup.hide(); // Hide the new popup until it is finished and sized.
+
   if (width) {
-    $popups.css('width', width);
+    $popup.css('width', width);
   }
-  $('body').append( $popups ); // Add the popups to the DOM.
+  
+  // Add the new popup to the DOM.
+  $('body').append($popup); 
 
-  // Adding button functions
+  // Add button function callbacks.
   if (buttons) {
-    jQuery.each(buttons, function (id, button) { 
-      $('#'+id).click(button.func);
-    });  
+    jQuery.each(buttons, function(id, button){
+      $('#' + id).click(button.func);
+    });
   }
-  $('#popups-close').click( Drupal.popups.close );
-  $('a.popups-close').click( Drupal.popups.close );
     
+  // Add the default click-to-close behavior.
+  popup.$popupClose().click(function(){
+    return Popups.close(popup);
+  });
+
+  Popups.resizeAndCenter(popup);
+
+  // Focus on the first input element in the popup window.
+  popup.refocus(); 
+  
+  // TODO - this isn't the place for this - should mirror addLoading calls.
+  // Remove the loading image.
+  Popups.removeLoading();
+   
+  return popup;
+};  
+
+/**
+ * Adjust the popup's height to fit it's content.
+ * Move it to be centered on the screen.
+ * This undoes the effects of popup.hide().
+ * 
+ * @param popup
+ */
+Popups.resizeAndCenter = function(popup) {
+  var $popup = popup.$popup();
+  
   // center on the screen, adding in offsets if the window has been scrolled
-  var popupWidth = $popups.width();  
-  var windowWidth = $(window).width();
-  var left = (windowWidth / 2) - (popupWidth / 2) + Drupal.popups.scrollLeft();
+  var popupWidth = $popup.width();  
+  var windowWidth = Popups.windowWidth();
+  var left = (windowWidth / 2) - (popupWidth / 2) + Popups.scrollLeft();
   
   // Get popups's height on the page.
-  // Need to do it this way, since we get visible flash in FF2 if popups is not visible!
-  var popupHeight = $popups.height(); 
-  var windowHeight = $(window).height();
+  $popup.css('height', 'auto'); // Reset height.
+  var popupHeight = $popup.height(); 
+  $popup.height(popupHeight);
+  var windowHeight = Popups.windowHeight();
+   
   if (popupHeight > (0.9 * windowHeight) ) { // Must fit in 90% of window.
     popupHeight = 0.9 * windowHeight;
-    $popups.height(popupHeight);
+    $popup.height(popupHeight);
   }  
-  var top = (windowHeight / 2) - (popupHeight / 2) + Drupal.popups.scrollTop();
+  var top = (windowHeight / 2) - (popupHeight / 2) + Popups.scrollTop();
 
-  $popups.css('top', top).css('left', left); // Position the popups to be visible.
+  $popup.css('top', top).css('left', left); // Position the popups to be visible. 
+};
   
-  this.refocus(); // TODO: capture the focus when it leaves the dialog.
-  Drupal.popups.removeLoading(); // Remove the loading img.
-   
-  return false;
-};
 
 /**
- *  Simple popups that functions like the browser's alert box.
+ *  Create and show a simple popup dialog that functions like the browser's alert box.
  */
-Drupal.popups.message = function(title, message) {
+Popups.message = function(title, message) {
   message = message || '';
+  var popup = new Popups.Popup();
   var buttons = {
-    'popup_ok': {title: Drupal.t('OK'), func: Drupal.popups.close}
+    'popup_ok': {title: Drupal.t('OK'), func: function(){popup.close();}}
   };
-  Drupal.popups.open(title, message, buttons);
+  popup.open(title, message, buttons);
+  return popup;
 };
 
 /**
  * Handle any special keys when popups is active.
  */
-Drupal.popups.keyHandle = function(e) {
+Popups.keyHandle = function(e) {
   if (!e) {
     e = window.event;
   }
   switch (e.keyCode) {
     case 27: // esc
-      Drupal.popups.close();
+      Popups.close();
       break;
     case 191: // '?' key, show help.
       if (e.shiftKey && e.ctrlKey) {
@@ -179,7 +536,7 @@
           $help.click();
         }
         else {
-          Drupal.popups.message(Drupal.t("Sorry, there is no additional help for this page"));
+          Popups.message(Drupal.t("Sorry, there is no additional help for this page"));
         }
       }
       break;
@@ -189,162 +546,334 @@
 /*****************************************************************************
  * Appearence Functions (overlay, loading graphic, remove popups)     *********
  *****************************************************************************/
-
-Drupal.popups.removePopup = function() {
-  $('#popups').remove();  
-}; 
  
-Drupal.popups.addOverlay = function() {
+/**
+ * Add full page div between the page and the dialog, to make the popup modal.
+ */
+Popups.addOverlay = function() {
   var $overlay = $('#popups-overlay');
-  if (!$overlay.size()) { // Overlay does not already exist, so create it.
+  if (!$overlay.length) { // Overlay does not already exist, so create it.
     $overlay = $(Drupal.theme('popupOverlay'));
     $overlay.css('opacity', '0.4'); // for ie6(?)
     // Doing absolute positioning, so make overlay's size equal the entire body.
-    $doc = $(document);
+    var $doc = $(document);
     $overlay.width($doc.width()).height($doc.height()); 
-    $overlay.click(Drupal.popups.close);
+    $overlay.click(function(){Popups.close();});
     $('body').prepend($overlay);
   }
 };
 
-Drupal.popups.removeOverlay = function() {
-  $('#popups-overlay').remove();
+/**
+ * Remove overlay if popupStack is empty.
+ */
+Popups.removeOverlay = function() {
+  if (!Popups.popupStack.length) {
+    $('#popups-overlay').remove();
+  }
 };
 
-Drupal.popups.addLoading = function() {
+/**
+ * Add a "Loading" message while we are waiting for the ajax response.
+ */
+Popups.addLoading = function() {
   var $loading = $('#popups-loading');
-  if (!$loading.size()) { // Overlay does not already exist, so create it.
-    var waitImageSize = 100;
-    var left = ($(window).width() / 2) - (waitImageSize / 2)  + Drupal.popups.scrollLeft();
-    var top = ($(window).height() / 2) - (waitImageSize / 2)  + Drupal.popups.scrollTop();
-    $loading = $( Drupal.theme('popupLoading', left, top) );
-    $('body').prepend($loading);
+  if (!$loading.length) { // Loading image does not already exist, so create it.
+    $loading = $(Drupal.theme('popupLoading'));
+    $('body').prepend($loading); // Loading div is initially display:none.
+    var width = $loading.width();
+    var height = $loading.height();
+    var left = (Popups.windowWidth() / 2) - (width / 2) + Popups.scrollLeft();
+    var top = (Popups.windowHeight() / 2) - (height / 2) + Popups.scrollTop();
+    $loading.css({'top': top, 'left': left, 'display': 'block'}); // Center it and make it visible.
   }
 };
 
-Drupal.popups.removeLoading = function() {
+Popups.removeLoading = function() {
   $('#popups-loading').remove();
 };
 
+// Should I fold this function into Popups.pop?
+Popups.removePopup = function(popup) {  
+  if (!Popups.isset(popup)) {
+    popup = Popups.activePopup();
+  }
+  if (popup) {
+    popup.$popup().remove();
+    Popups.popupStack.splice(jQuery.inArray(popup,Popups.popupStack), 1); // Remove popup from stack.  Probably should rework into .pop()
+  }  
+}; 
+
 /**
  * Remove everything.
  */
-Drupal.popups.close = function() {
-  Drupal.popups.removePopup();
-  Drupal.popups.removeLoading();
-  Drupal.popups.removeOverlay();
+Popups.close = function(popup) {
+  if (!Popups.isset(popup)) {
+    popup = Popups.activePopup();
+  }
+  Popups.removePopup(popup);  // Should this be a pop??
+  Popups.removeLoading();
+  if (Popups.activePopup()) {
+    Popups.activePopup().show();
+    Popups.activePopup().refocus();
+  }
+  else {
+    Popups.removeOverlay();
+    Popups.restorePage();
+  }
   return false;
 };
 
 /**
- * Set the focus on the popups to the first visible form element, or the first button, or the close link.
+ * Save the page's original Drupal.settings.
  */
-Drupal.popups.refocus = function() {
-  $focus = $('#popups input:visible:eq(0)');
-  if (!isset(focus)) {
-    $focus = $('#popups-close'); // Doesn't seem to work.
+Popups.saveSettings = function() {
+  if (!Popups.originalSettings) {
+    Popups.originalSettings = Drupal.settings;
   }
-  $focus.focus();
 };
 
+/**
+ * Restore the page's original Drupal.settings.
+ */
+Popups.restoreSettings = function() {
+  Drupal.settings = Popups.originalSettings;  
+};
+
+/**
+ * Remove as much of the effects of jit loading as possible.
+ */
+Popups.restorePage = function() {
+  Popups.restoreSettings();
+  // Remove the CSS files that were jit loaded for popup.
+  for (var i in Popups.addedCSS) {
+    var link = Popups.addedCSS[i];
+    $('link[href='+ $(link).attr('href') + ']').remove();
+  }
+  Popups.addedCSS = [];
+};
+
+
 /****************************************************************************
- * Theme Functions   ********************************************************
+ * Utility Functions   ******************************************************
  ****************************************************************************/
 
-Drupal.theme.prototype.popupLoading = function(left, top) {
-  var loading = '<div id="popups-loading">';
-  loading += '<div style="left:' + left +'px; top:' + top +'px;">';
-  loading += '<img src="'+ Drupal.settings.basePath + Drupal.settings.popups.modulePath + '/ajax-loader.gif" />';
-  loading += '</div></div>';
-  return loading;
+/**
+ * Get the position of the left side of the browser window.
+ */
+Popups.scrollLeft = function() {
+  return Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
 };
 
-Drupal.theme.prototype.popupOverlay = function() {
-  return '<div id="popups-overlay"></div>';
+/**
+ * Get the position of the top of the browser window.
+ */
+Popups.scrollTop = function() {
+  return Math.max(document.documentElement.scrollTop, document.body.scrollTop);
 };
 
-Drupal.theme.prototype.popupButton = function(title, id) {
-  return '<input type="button" value="'+ title +'" id="'+ id +'" />';
+/**
+ * Get the height of the browser window.
+ * Fixes jQuery & Opera bug - http://drupal.org/node/366093
+ */
+Popups.windowHeight = function() {
+  if ($.browser.opera && $.browser.version > "9.5" && $.fn.jquery <= "1.2.6") { 
+    return document.documentElement.clientHeight;
+  }
+  return $(window).height();
 };
 
-Drupal.theme.prototype.popupDialog = function(title, body, buttons) {
-  var template = Drupal.settings.popups.template;
-  var popups = template.replace('%title', title).replace('%body', body);
-  
-  var themedButtons = '';
-  if (buttons) {
-    jQuery.each(buttons, function (id, button) { 
-      themedButtons += Drupal.theme('popupButton', button.title, id);
-    });  
-  }  
-  popups = popups.replace('%buttons', themedButtons);  
-  return popups;
+/**
+ * Get the height of the browser window.
+ * Fixes jQuery & Opera bug - http://drupal.org/node/366093
+ */
+Popups.windowWidth = function() {
+  if ($.browser.opera && $.browser.version > "9.5" && $.fn.jquery <= "1.2.6") { 
+    return document.documentElement.clientWidth;
+  }
+  return $(window).width();
 };
 
-// Stolen jQuery offset
-Drupal.popups.scrollLeft = function() {
-  return Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
+Popups.nextCounter = function() {
+  if (this.counter === undefined) {
+    this.counter = 0;
+  }
+  else {
+    this.counter++;
+  }
+  return this.counter;
 };
 
-// Stolen jQuery offset
-Drupal.popups.scrollTop = function() {
-  return Math.max(document.documentElement.scrollTop, document.body.scrollTop);
+/****************************************************************************
+ * Ajax Functions   ******************************************************
+ ****************************************************************************/
+
+/**
+ * Add additional CSS to the page.
+ */
+Popups.addCSS = function(css) {
+  Popups.addedCSS = [];
+  for (var type in css) {
+    for (var file in css[type]) {
+      var link = css[type][file];
+      // Does the page already contain this stylesheet?
+      if (!$('link[href='+ $(link).attr('href') + ']').length) {
+        //$('head').append(link);
+        Popups.addedCSS.push(link); // Keep a list, so we can remove them later.
+      }
+    }
+  }
 };
 
+/**
+ * Add additional Javascript to the page.
+ */
+Popups.addJS = function(js) {
+  // Parse the json info about the new context.
+  var scripts = [];
+  var inlines = [];
+  for (var type in js) {
+    if (type != 'setting') {
+      var Reg_jquery = new RegExp('jquery');
+      var Reg_drupal = new RegExp('drupal');
+      for (var file in js[type]) {
+        if (type == 'inline') {
+          inlines.push($(js[type][file]).text());
+        }
+        else {
+          var src = $(js[type][file]).attr('src');
+          if (!Reg_jquery.test(src) && !Reg_drupal.test(src)) {
+            scripts.push(src);
+          }
+        }
+      }
+    }
+  }
+
+  // Add new JS settings to the page, needed for #ahah properties to work.
+  Drupal.settings = js.setting;
+
+  for (var i in scripts) {
+    var src = scripts[i];
+    if (!$('script[src='+ src + ']').length && !Popups.addedJS[src]) {
+      // Get the script from the server and execute it.
+      $.ajax({ 
+        type: 'GET',
+        url: src,
+        dataType: 'script',
+        async : false,
+        success: function(script) {
+          eval(script);
+        }
+      });
+      // Mark the js as added to the underlying page.
+      Popups.addedJS[src] = true;
+    }
+  }
+
+  return inlines;
+};
+
+/**
+ * Execute the jit loaded inline scripts.
+ * Q: Do we want to re-excute the ones already in the page?
+ * 
+ * @param inlines
+ *   Array of inline scripts.
+ */
+Popups.addInlineJS = function(inlines) {
+  // Load the inlines into the page.
+  for (var n in inlines) {
+    // If the script is not already in the page, execute it.
+    if (!$('script:not([src]):contains(' + inlines[n] + ')').length) {
+      eval(inlines[n]);
+    }
+  }
+};
+
+Popups.beforeSend = function(xhr) {
+  xhr.setRequestHeader("X-Drupal-Render-Mode", 'json/popups');
+};
+
+/**
+ * Do before the form in the popups is submitted.
+ */
+Popups.beforeSubmit = function(formData, $form, options) {
+  Popups.removePopup(); // Remove just the dialog, but not the overlay.
+  Popups.addLoading();
+};
+
+
 /****************************************************************************
  * Page & Form in popups functions                                         ***
  ****************************************************************************/
 
 /**
- * Use Ajax to open the link in a popups window.
+ * Use Ajax to open a link in a popups window.
  *
  * @param element
  *   Element that was clicked to open the popups.
  * @param options
  *   Hash of options controlling how the popups interacts with the underlying page.
+ * @param parent
+ *   If path is being opened from inside another popup, that popup is the parent.
  */
-Drupal.popups.openPath = function(element, options) {
-  // let the user know something is happening
+Popups.openPath = function(element, options, parent) {
+  Popups.saveSettings();
+
+  // Let the user know something is happening.
   $('body').css("cursor", "wait");
   
   // TODO - get nonmodal working.
   if (!options.nonModal) {
-    Drupal.popups.addOverlay(); 
+    Popups.addOverlay(); 
   }
-  Drupal.popups.addLoading();
+  Popups.addLoading();
   
   var href = options.href ? options.href : element.href;
-  $(document).trigger('popups_open_path', [element, href]); // Broadcast "Popups Open Path" event.
+  $(document).trigger('popups_open_path', [element, href]); // Broadcast Popup Open Path event.
   
   var params = {}; 
-  // Force the popups to return back to the orignal page when forms are done, unless forceReturn option is set.
-  if (!options.forceReturn) { // If forceReturn, requestor wants data from different page.
+  // Force the popups to return back to the orignal page when forms are done, unless hijackDestination option is set to FALSE.
+  if (options.hijackDestination) { 
+    var returnPath;
+    if (parent) {
+      returnPath = parent.path;
+    }
+    else { // No parent, so bring flow back to original page.
+      returnPath = Popups.originalSettings.popups.originalPath;
+    }    
     href = href.replace(/destination=[^;&]*[;&]?/, ''); // Strip out any existing destination param.
-    params.destination = Drupal.settings.popups.originalPath; // Set the destination to the original page.    
+    params.destination = returnPath; // Set the destination to return to the parent's path.    
   }
 
-  ajaxOptions = {
+  var ajaxOptions = {
     url: href,
     dataType: 'json',
     data: params,
-    beforeSend: Drupal.popups.beforeSend,
+    beforeSend: Popups.beforeSend,
     success: function(json) { 
-      Drupal.popups.openContent(json.title, json.messages + json.content, options, element);
-      $(document).trigger('popups_open_path_done', [element, href]); // Broadcast "Popups Open Path Done" event.
+      // Add additional CSS to the page.
+      Popups.addCSS(json.css);
+      var inlines = Popups.addJS(json.js);
+      var popup = Popups.openPathContent(json.path, json.title, json.messages + json.content, element, options, parent);
+      Popups.addInlineJS(inlines);   
+      // Broadcast an event that the path was opened.
+      $(document).trigger('popups_open_path_done', [element, href, popup]);  
     },
     complete: function() {
       $('body').css("cursor", "auto"); // Return the cursor to normal state.      
     }
   };
 
+  var ajaxOptions;
   if (options.reloadOnError) {
     ajaxOptions.error = function() {
       location.reload(); // Reload on error. Is this working?
     };    
   }
   else {
-   ajaxOptions.error = function() {
-      Drupal.popups.message("Unable to open: " + href);
+    ajaxOptions.error = function() {
+      Popups.message("Unable to open: " + href);
     };
   }
   $.ajax(ajaxOptions);
@@ -353,166 +882,203 @@
 };
 
 /**
- * Open content in an ajax popups.
+ * Open path's content in an ajax popups.
  *
  * @param title
  *   String title of the popups.
  * @param content
  *   HTML to show in the popups.
+ * @param element
+ *   A DOM object containing the element that was clicked to initiate the popup. 
  * @param options
  *   Hash of options controlling how the popups interacts with the underlying page.
- * @param element
- *   A DOM object containing the element that was clicked to initiate the popup. 
+ * @param parent
+ *   Spawning popup, or null if spawned from original page. 
  */
-Drupal.popups.openContent = function(title, content, options, element) {
-  Drupal.popups.open(title, content, null, options.width); 
+Popups.openPathContent = function(path, title, content, element, options, parent) {  
+  var popup = new Popups.Popup();
+  Popups.open(popup, title, content, null, options.width); 
+
+  // Set properties on new popup.  
+  popup.parent = parent;
+  popup.path = path;
+  popup.options = options;
+  popup.element = element;
+
   // Add behaviors to content in popups. 
-  // TODO: d-n-d: need to click to let go of selection.
   delete Drupal.behaviors.tableHeader; // Work-around for bug in tableheader.js (http://drupal.org/node/234377)
   delete Drupal.behaviors.teaser; // Work-around for bug in teaser.js (sigh).
-  Drupal.attachBehaviors($('#popups-body'));
+  Drupal.attachBehaviors(popup.$popupBody());
   // Adding collapse moves focus.
-  Drupal.popups.refocus();
+  popup.refocus();
 
   // If the popups contains a form, capture submits.
-  var $form = $('form', '#popups-body');
-  $form.ajaxForm({   
-    dataType: 'json',   
-    beforeSubmit: Drupal.popups.beforeSubmit,
-    beforeSend: Drupal.popups.beforeSend,
-    success: function(response, status) {
-      Drupal.popups.formSuccess(response, options, element);
-    },
-    error: function() {
-      Drupal.popups.message("Bad Response form submission");
-    }
-  });
+  var $form = $('form', popup.$popupBody());
+  if ($form.length) {
+    $form.ajaxForm({   
+      dataType: 'json',   
+      beforeSubmit: Popups.beforeSubmit,
+      beforeSend: Popups.beforeSend,
+      success: function(json, status) {
+        Popups.formSuccess(popup, json);
+      },
+      error: function() {
+        Popups.message("Bad Response form submission");
+      }
+    });
+  }
+  return popup;
 };
 
-Drupal.popups.beforeSend = function(xhr) {
-  xhr.setRequestHeader("X-Drupal-Render-Mode", 'json/popups');
-};
-
 /**
- * Do before the form in the popups is submitted.
- */
-Drupal.popups.beforeSubmit = function(formData, $form, options) {
-  Drupal.popups.removePopup(); // Remove just the dialog, but not the overlay.
-  Drupal.popups.addLoading();
-};
-
-/**
  * The form in the popups was successfully submitted
  * Update the originating page.
- * Show any messages in a popups (TODO - make this a configurable option).
+ * Show any messages in a popups.
  * 
- * @param response
+ * @param popup
+ *   The popup object that contained the form that was just submitted.
+ * @param data
  *   JSON object from server with status of form submission.
- * @param options
- *   Hash of options controlling how the popups interacts with the underlying page.
- *     noUpdate: bool, does the popups effect the underlying page.
- *     nonModal: bool, does the popups block access to the underlying page.
- *     targetSelectors: hash of jQuery selectors, overrides defaultTargetSelector.
- *     titleSelectors: array of jQuery selectors, where to put the the new title of the page.
- * @param element
- *   A DOM object containing the element that was clicked to initiate the popup. 
  */
-Drupal.popups.formSuccess = function(data, options, element) {  
+Popups.formSuccess = function(popup, data) {    
   // Determine if we are at an end point, or just moving from one popups to another.
-  var done = (data.path === Drupal.settings.popups.originalPath) || (data.path === options.forceReturn);
+  var done = popup.isDone(data.path);
   if (!done) { // Not done yet, so show new page in new popups.
-    Drupal.popups.removeLoading();
-    Drupal.popups.openContent(data.title, data.messages + data.content, options, element);
+    Popups.removeLoading();
+    Popups.openPathContent(data.path, data.title, data.messages + data.content, popup.element, popup.options, popup.parent);
   }
-  else { // Done.
-    // Execute the afterSubmit callback if available.
-    if (options.afterSubmit) { 
-      var afterSubmitResult = eval(options.afterSubmit +'(data, options, element)');
-      if (afterSubmitResult == false) { // Give afterSubmit callback a chance to skip normal processing.
+  else { // We are done with popup flow.
+    // Execute the onUpdate callback if available.
+    if (popup.options.updateMethod === 'callback' && popup.options.onUpdate) { 
+      var result = eval(popup.options.onUpdate +'(data, popup.options, popup.element)');
+      if (result === false) { // Give onUpdate callback a chance to skip normal processing.
         return;
       }
     }
 
-    if (options.reloadWhenDone) { // Force a complete, non-ajax reload of the page.
-      location.reload(); 
+    if (popup.options.updateMethod === 'reload') { // Force a complete, non-ajax reload of the page.
+      if (popup.options.updateSource === 'final') {
+        location.href = Drupal.settings.basePath + data.path; // TODO: Need to test this.
+      }
+      else { // Reload originating page.
+        location.reload(); 
+      }
     }
     else { // Normal, targeted ajax, reload behavior.
-      // show messages in dialog and embed the results in the original page.
-      var showMessage = data.messages.length && !options.noMessage;
-      if (showMessage) {
-        Drupal.popups.message(data.messages);
-        if (!Drupal.settings.popups.popupFinalMessage) {
-          setTimeout(Drupal.popups.close, 2500); // Autoclose the message box in 2.5 seconds.
+      var showingMessagePopup = false;
+      // Show messages in dialog and embed the results in the original page.
+      // TODO - should seperate these two functions.
+//      var showMessage = data.messages.length && !popup.options.noMessage;
+      if (data.messages.length) {
+        // If we just dismissed the last popup dialog.
+        if (!Popups.activePopup() && !popup.options.noMessage) {
+          // Show drupal_set_message in message popup.
+          var messagePopup = Popups.message(data.messages); 
+          if (Popups.originalSettings.popups.autoCloseFinalMessage) {
+            setTimeout(function(){Popups.close(messagePopup);}, 2500); // Autoclose the message box in 2.5 seconds.
+          }
+          showingMessagePopup = true;
         }
-  
-        // Insert the message into the page above the content.
+    
+          
+        // Insert the message into the parent layer, above the content.
         // Might not be the standard spot, but it is the easiest to find.
-        var $next = $(Drupal.settings.popups.defaultTargetSelector);
-        $next.parent().find('div.messages').remove(); // Remove the current messages.
-        $next.before(data.messages);
+        var $next;
+        if (popup.targetLayerSelector() === 'body') {
+          $next = $('body').find(Popups.originalSettings.popups.defaultTargetSelector);
+        }
+        else {
+          $next = $(popup.targetLayerSelector()).find('.popups-body');
+        }
+        $next.parent().find('div.messages').remove(); // Remove the existing messages.
+        $next.before(data.messages); // Insert new messages.
       }
           
       // Update the content area (defined by 'targetSelectors').
-      if (!options.noUpdate) { 
-        Drupal.popups.testContentSelector();
-        if (isset(options.targetSelectors)) { // Pick and choose what returned content goes where.
-          jQuery.each(options.targetSelectors, function(t_new, t_old) {
+      if (popup.options.updateMethod !== 'none') { 
+        Popups.testContentSelector(); // Kick up warning message if selector is bad.
+
+        Popups.restoreSettings(); // Need to restore original Drupal.settings.popups.links before running attachBehaviors.  This probably has CSS side effects!        
+        if (popup.options.targetSelectors) { // Pick and choose what returned content goes where.
+          jQuery.each(popup.options.targetSelectors, function(t_new, t_old) {
             if(!isNaN(t_new)) {
               t_new = t_old; // handle case where targetSelectors is an array, not a hash.
             }
             var new_content = $(t_new, data.content);
-            var $c = $(t_old).html(new_content); // Inject the new content into the original page.
-            Drupal.attachBehaviors($c);  
+            var $c = $(popup.targetLayerSelector()).find(t_old).html(new_content); // Inject the new content into the original page.
+
+            Drupal.attachBehaviors($c);
           });
         }
         else { // Put the entire new content into default content area.
-          $c = $(Drupal.settings.popups.defaultTargetSelector).html(data.content);
+          var $c = $(popup.targetLayerSelector()).find(Popups.originalSettings.popups.defaultTargetSelector).html(data.content);
           Drupal.attachBehaviors($c);                    
         }
       }
       
       // Update the title of the page.
-      if (isset(options.titleSelectors)) {
-        jQuery.each(options.titleSelectors, function() {
+      if (popup.options.titleSelectors) {
+        jQuery.each(popup.options.titleSelectors, function() {
           $(''+this).html(data.title);
         });
       }
               
       // Done with changes to the original page, remove effects.
-      Drupal.popups.removeLoading();
-      if (!showMessage) { 
-        // If there is not a messages popups, so remove the overlay.
-        Drupal.popups.removeOverlay();
+      Popups.removeLoading();
+      if (!showingMessagePopup) { 
+        // If there is not a messages popups, pop the stack.
+        // Sending in null to Popups.close reveales the next popup in the stack.
+        // If the stack is empty, it will remove the overlay.
+        Popups.close(null);
       }
     }
     
     // Broadcast an event that popup form was done and successful.
-    $(document).trigger('popups_form_success', [element]);
+    $(document).trigger('popups_form_success', [popup]);
     
-  }  // End of updating original page.
+  }  // End of updating spawning layer.
 }; 
 
+
 /**
+ * Get a jQuery object for the content of a layer.
+ * @param layer
+ *   Either a popup, or null to signify the original page.
+ */
+Popups.getLayerContext = function(layer) {  
+  var $context;
+  if (!layer) {
+    $context = $('body').find(Popups.originalSettings.popups.defaultTargetSelector);
+  }
+  else {
+    $context = layer.$popupBody();
+  }
+  return $context;
+}
+
+/**
  * Submit the page and reload the results, before popping up the real dialog.
  *
  * @param element
- *   Element that was clicked to open the popups.
+ *   Element that was clicked to open a new popup.
  * @param options
  *   Hash of options controlling how the popups interacts with the underlying page.
+ * @param layer
+ *   Popup with form to save, or null if form is on original page. 
  */
-Drupal.popups.savePage = function(element, options) {
-  var target = Drupal.settings.popups.defaultTargetSelector;
-  var $form = $('form', target);
+Popups.saveFormOnLayer = function(element, options, layer) {
+  var $context = Popups.getLayerContext(layer);
+  var $form = $context.find('form');
   var ajaxOptions = {
     dataType: 'json',
-    beforeSubmit: Drupal.popups.beforeSubmit,   
-    beforeSend: Drupal.popups.beforeSend,
+    beforeSubmit: Popups.beforeSubmit,   
+    beforeSend: Popups.beforeSend,
     success: function(response, status) { 
       // Sync up the current page contents with the submit.
-      var $c = $(target).html(response.content); // Inject the new content into the page.
+      var $c = $context.html(response.content); // Inject the new content into the page.
       Drupal.attachBehaviors($c);
-      // The form has been saved, the page reloaded, now safe to show the link in a popup.
-      Drupal.popups.openPath(element, options); 
+      // The form has been saved, the page reloaded, now safe to show the triggering link in a popup.
+      Popups.openPath(element, options, layer); 
     } 
   };
   $form.ajaxSubmit(ajaxOptions); // Submit the form. 
@@ -522,11 +1088,11 @@
  * Warn the user if ajax updates will not work
  *   due to mismatch between the theme and the theme's popup setting.
  */
-Drupal.popups.testContentSelector = function() {
-  var target = Drupal.settings.popups.defaultTargetSelector;
+Popups.testContentSelector = function() {
+  var target = Popups.originalSettings.popups.defaultTargetSelector;
   var hits = $(target).length;
   if (hits !== 1) { // 1 is the corrent answer.
-    msg = Drupal.t('The popup content area for this theme is misconfigured.') + '\n';
+    var msg = Drupal.t('The popup content area for this theme is misconfigured.') + '\n';
     if (hits === 0) {
       msg += Drupal.t('There is no element that matches ') + '"' + target + '"\n';
     }
@@ -537,3 +1103,53 @@
     alert(msg);
   }
 };
+
+
+// ****************************************************************************
+// * Theme Functions   ********************************************************
+// ****************************************************************************
+
+Drupal.theme.prototype.popupLoading = function() {
+  var loading = '<div id="popups-loading">';
+  loading += '<img src="'+ Drupal.settings.basePath + Popups.originalSettings.popups.modulePath + '/ajax-loader.gif" />';
+  loading += '</div>';
+  return loading;
+};
+
+Drupal.theme.prototype.popupOverlay = function() {
+  return '<div id="popups-overlay"></div>';
+};
+
+Drupal.theme.prototype.popupButton = function(title, id) {
+  return '<input type="button" value="'+ title +'" id="'+ id +'" />';
+};
+
+Drupal.theme.prototype.popupDialog = function(popupId, title, body, buttons) {
+  var template = Drupal.theme('popupTemplate', popupId);
+  var popups = template.replace('%title', title).replace('%body', body);
+  
+  var themedButtons = '';
+  if (buttons) {
+    jQuery.each(buttons, function (id, button) { 
+      themedButtons += Drupal.theme('popupButton', button.title, id);
+    });  
+  }  
+  popups = popups.replace('%buttons', themedButtons);  
+  return popups;
+};
+
+Drupal.theme.prototype.popupTemplate = function(popupId) {
+  var template;
+  template += '<div id="'+ popupId + '" class="popups-box">';
+  template += '  <div class="popups-title">';
+  template += '    <div class="popups-close"><a href="#">' + Drupal.t('Close') + '</a></div>';
+  template += '    <div class="title">%title</div>';
+  template += '    <div class="clear-block"></div>';
+  template += '  </div>';
+  template += '  <div class="popups-body">%body</div>';
+  template += '  <div class="popups-buttons">%buttons</div>';
+  template += '  <div class="popups-footer"></div>';
+  template += '</div>';
+  return template;
+};
+
