Index: skeleton_instance.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton_instance.inc,v
retrieving revision 1.10
diff -u -r1.10 skeleton_instance.inc
--- skeleton_instance.inc	7 Jan 2009 17:38:58 -0000	1.10
+++ skeleton_instance.inc	10 Jan 2009 18:23:44 -0000
@@ -185,7 +185,8 @@
         '#value' => locale_language_name($node->language),
       );
     }
-    $form['current_templates'][$i]['parent'] = array('#type' => 'select',
+    $form['current_templates'][$i]['parent'] = array(
+      '#type' => 'select',
       '#default_value' => $template->parent,
       '#options' => $options,
     );
@@ -398,6 +399,7 @@
     $output .= '<p>' . t('You must first assign at least one template to this skeleton outline. <a href="@edit-url">Edit this skeleton</a> to add a template to it.', array('@edit-url' => url('admin/content/skeleton/skeleton/' . $skeleton->skeleton_id . '/edit'))) . '</p>';
     return $output;
   }
+  drupal_add_js(drupal_get_path('module', 'skeleton') . '/skeleton-admin.js');
   $output .= drupal_get_form('skeleton_create_instance_form', $skeleton);
   return $output;
 }
@@ -405,57 +407,55 @@
 /**
  * FormsAPI for skeleton_create_instance()
  */
-function skeleton_create_instance_form($form_state, $skeleton) {
+function skeleton_create_instance_form(&$form_state, $skeleton) {
   $form = array();
   $form['instance'] = array(
     '#type' => 'fieldset',
     '#title' => t('Skeleton Information'),
     '#collapsible' => TRUE
   );
-  $form['instance']['title'] = array(
-    '#type' => 'textfield',
-    '#size' => 40,
-    '#title' => t('Book title'),
-    '#required' => TRUE,
-    '#description' => t('The title of the new book you wish to create'),
-  );
-  if (module_exists('translation')) {
-    // We are always creating a book page. This is modified from
-    // locale_form_alter().
-    if (variable_get('language_content_type_book', 0)) {
-      $form['instance']['language'] = array(
-        '#type' => 'select',
-        '#title' => t('Language'),
-        '#options' => array('' => t('Language neutral')) + locale_language_list('name'),
-      );
+
+  // Count the number of items with the parent set to 0. If this is more than
+  // one, we can't have a template as the book introduction.
+  $templates = skeleton_get_tree($skeleton->skeleton_id);
+  $parents_count = 0;
+  foreach($templates as $template) {
+    if ($template->parents[0] == 0) {
+      $parents_count++;
     }
-    // Node type without language selector: assign the default for new nodes
-    else {
-      $default = language_default();
-      $form['instance']['language'] = array(
+    if ($parents_count > 1) {
+      $form['instance']['introduction_enabled'] = array(
         '#type' => 'value',
-        '#value' => $default->language
+        '#value' => TRUE,
       );
+      break;
     }
   }
-  $form['instance']['body'] = array(
-    '#type' => 'textarea',
-    '#cols' => 40,
-    '#rows' => 10,
-    '#title' => t('Book introduction'),
-    '#required' => TRUE,
-    '#description' => t('A description of the contents of this book.'),
-    '#suffix' => t('<p>After you create this instance, you will be able to edit the complete book page.</p>'),
-  );
+  if ($parents_count < 2 && (!$form_state['add_introduction'])) {
+    $form['instance']['add_introduction'] = array(
+      '#type' => 'submit',
+      '#value' => t('Toggle custom introduction for this book'),
+      '#description' => t('This will add an introduction page to this book, and all templates will be made a child of the introduction page.'),
+      '#ahah' => array(
+        'path' => 'skeleton/introduction',
+        'effect' => 'slide',
+        'wrapper' => 'introduction-wrapper',
+      ),
+      '#submit' => array('skeleton_add_introduction_submit'), // If no javascript action.
+      '#suffix' => '<div id="introduction-wrapper" style="clear: both;" />',
+      '#weight' => -100,
+    );
+  }
+  else {
+    $form['instance'] += skeleton_introduction_form($form_state);
+  }
   $form['instance']['skeleton_id'] = array(
     '#type' => 'value',
     '#value' => $skeleton->skeleton_id,
   );
 
-  // First, we need to determine if any skeleton tokens are used in this skeleton instance.
-  $templates = skeleton_get_tree($skeleton->skeleton_id);
-
-  // Fetch all of the custom tokens.
+  // We need to determine if any skeleton tokens are used in this skeleton
+  // instance.
   $skeleton_tokens = array();
   $result = db_query("SELECT * FROM {skeleton_token}");
   while ($token = db_fetch_object($result)) {
@@ -488,7 +488,7 @@
         '#type' => 'textfield',
         '#title' => $token->token,
         '#description' => $token->description,
-        '#required' => TRUE,
+        '#default_value' => isset($form_state['values']['skeleton_tokens'][$token->token]) ? $form_state['values']['skeleton_tokens'][$token->token] : "",
       );
     }
   }
@@ -500,6 +500,157 @@
 }
 
 /**
+ * Forms API validation function for the create instance form. We need this so
+ * we don't throw errors for required fields when changing the introduction
+ * status. A better way would be a way to intercept / override #required in
+ * specific cases, but the Forms API doesn't support that.
+ * 
+ * @param $form
+ *   The form being validated.
+ * @param $form_state
+ *   The current state of the form.
+ */
+function skeleton_create_instance_form_validate($form, &$form_state) {
+  if (!($form_state['clicked_button']['#value'] == t('Create new book from skeleton'))) {
+    return;
+  }
+  if (isset($form['instance']['title'])) {
+    if (empty($form_state['values']['title'])) {
+      form_set_error('title', t('You must set a title for the custom introduction.'));
+    }
+    if (empty($form_state['values']['body'])) {
+      form_set_error('body', t('You must set a body for the custom introduction.'));
+    }
+  }
+  // Now, do token validation.
+  foreach(element_children($form['skeleton_tokens']) as $token) {
+    if (empty($form_state['values']['skeleton_tokens'][$token])) {
+      form_set_error("skeleton_tokens][$token", t('You must set a value for the %token token.', array('%token' => $token)));
+    }
+  }
+}
+
+/**
+ * Return a Form API array of the introduction form.
+ *
+ * @param $form_state
+ *   The current state of the form.
+ * @return
+ *   The Form API array.
+ */
+function skeleton_introduction_form($form_state) {
+  // Add a remove button for the JS-disabled case.
+  if ($form_state['add_introduction']) {
+    $form['remove_introduction'] = array(
+      '#type' => 'submit',
+      '#value' => t('Remove introduction to this book'),
+      '#submit' => array('skeleton_add_introduction_submit'), // If no javascript action.
+      '#weight' => -100,
+    );
+  }
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#size' => 40,
+    '#title' => t('Book title'),
+    '#description' => t('The title of the new book you wish to create'),
+    '#weight' => -10,
+  );
+  if (module_exists('translation')) {
+    // We are always creating a book page. This is modified from
+    // locale_form_alter().
+    if (variable_get('language_content_type_book', 0)) {
+      $form['language'] = array(
+        '#type' => 'select',
+        '#title' => t('Language'),
+        '#options' => array('' => t('Language neutral')) + locale_language_list('name'),
+        '#weight' => -5,
+      );
+    }
+    // Node type without language selector: assign the default for new nodes
+    else {
+      $default = language_default();
+      $form['language'] = array(
+        '#type' => 'value',
+        '#value' => $default->language
+      );
+    }
+  }
+  $form['body'] = array(
+    '#type' => 'textarea',
+    '#cols' => 40,
+    '#rows' => 10,
+    '#title' => t('Book introduction'),
+    '#description' => t('A description of the contents of this book.'),
+    '#suffix' => t('<p>After you create this instance, you will be able to edit the complete book page.</p>'),
+    '#weight' => 0,
+  );
+  
+  return $form;
+}
+
+/**
+ * Submit handler to enable a custom introduction to a book. This handler is
+ * used when javascript is not available. It makes changes to the form state
+ * and the entire form is rebuilt during the page reload.
+ */
+function skeleton_add_introduction_submit($form, &$form_state) {
+  // Set the form to rebuild and run submit handlers.
+  // Make the changes we want to the form state.
+  if ($form_state['values']['add_introduction']) {
+    $form_state['add_introduction'] = TRUE;
+  }
+  $form_state['rebuild'] = TRUE;
+  $form = form_builder('skeleton_create_instance_form', $form, $form_state);
+}
+
+/**
+ * Enable or disable a custom introduction form for an instantiated book.
+ */
+function skeleton_introduction_js() {
+  // Build the new form
+  $form_state = array('submitted' => FALSE);
+  $form_build_id = $_POST['form_build_id'];
+  // Add the new element to the stored form. Without adding the element to the
+  // form, Drupal is not aware of this new elements existence and will not
+  // process it. We retreive the cached form, add the element, and resave.
+  if (!$form = form_get_cache($form_build_id, $form_state)) {
+    exit();
+  }
+  // Get the form element
+  if (!$form['instance']['introduction_added']) {
+    $form_element = skeleton_introduction_form($form_id);
+    // Let other modules alter it.
+    drupal_alter('form', $form_element, array(), 'skeleton_introduction_js');
+    $form['instance'] += $form_element;
+    $form['instance']['introduction_added'] = array(
+      '#type' => 'value',
+      '#value' => TRUE,
+    );
+  }
+  else {
+    unset($form['instance']['title']);
+    unset($form['instance']['body']);
+    unset($form['instance']['language']);
+    unset($form['instance']['introduction_added']);
+  }
+  form_set_cache($form_build_id, $form, $form_state);
+  $form += array(
+    '#post' => $_POST,
+    '#programmed' => FALSE,
+  );
+
+  // Rebuild the form.
+  $form = form_builder('skeleton_create_instance_form', $form, $form_state);
+
+  // Render the new output. 
+  $introduction_form = $form['instance'];
+  unset($introduction_form['add_introduction']);
+  unset($introduction_form['#type']);
+  $output = drupal_render($introduction_form);
+  drupal_json(array('status' => TRUE, 'data' => $output));
+}
+
+/**
  * FormsAPI for skeleton_create_instance()
  * Creates the nodes from the outline using drupal_execute().
  *
@@ -507,7 +658,6 @@
  * 
  */
 function skeleton_create_instance_form_submit($form, &$form_state) {
-  include_once drupal_get_path('module', 'node') . '/node.pages.inc';
   global $user;
   module_load_include('inc', 'node', 'node.pages');
   module_load_include('inc', 'skeleton', 'skeleton_token');
@@ -516,6 +666,26 @@
   if (!empty($nodes)) {
     $parents = array();
     // get the next node id and use it for the parent
+    if ($form_state['values']['add_introduction'] == 0) {
+      // Get the first template, use it's data.
+      $node = array();
+      $node['values'] = unserialize($nodes[0]->node_data);
+
+      // Replace custom tokens in the title and body fields. Replace skeleton
+      // tokens first so they may be used as node tokens such as [title].
+      $node['values']['title'] = token_replace($node['values']['title'], 'skeleton', $form_state['values']['skeleton_tokens']);
+      $node['values']['title'] = token_replace($node['values']['title'], 'node', (object)$form_state['values']);
+      $form_state['values']['title'] = $node['values']['title'];
+
+      $node['values']['body'] = token_replace($node['values']['body'], 'skeleton', $form_state['values']['skeleton_tokens']);
+      $node['values']['body'] = token_replace($node['values']['body'], 'node', (object)$form_state['values']);
+      $form_state['values']['body'] = $node['values']['body'];
+
+      $form_state['values']['language'] = $node['values']['language'];
+
+      // We've done this template, so remove it.
+      unset($nodes[0]);
+    }
     $parent_node = array(
       'values' => array(
         'uid' => $user->uid,
Index: skeleton.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton.module,v
retrieving revision 1.13
diff -u -r1.13 skeleton.module
--- skeleton.module	7 Jan 2009 17:38:58 -0000	1.13
+++ skeleton.module	10 Jan 2009 18:23:40 -0000
@@ -200,6 +200,12 @@
     'weight' => 10,
     'file' => 'skeleton_instance.inc',
   );
+  $items['skeleton/introduction'] = array(
+    'page callback' => 'skeleton_introduction_js',
+    'access arguments' => array('create new instances'),
+    'type' => MENU_CALLBACK,
+    'file' => 'skeleton_instance.inc',
+  );
 
   return $items;
 }
Index: skeleton-admin.js
===================================================================
RCS file: skeleton-admin.js
diff -N skeleton-admin.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ skeleton-admin.js	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,8 @@
+$(document).ready(function() {
+  // If JS is enabled, then override the submit handler so that enter presses
+  // on textfields don't trigger the no-JS fallback.
+  $("form#skeleton-create-instance-form #edit-add-introduction").click(function(){
+    $("form#skeleton-create-instance-form #edit-submit").click();
+    return false;
+  });
+});
