You are here

makemeeting.field.inc in Make Meeting Scheduler 7.2

This file is mostly about the field configuration.

File

makemeeting.field.inc
View source
<?php

/**
 * @file
 * This file is mostly about the field configuration.
 */

/**
 * Implements hook_field_info().
 */
function makemeeting_field_info() {
  return array(
    'makemeeting' => array(
      'label' => t('Makemeeting form'),
      'description' => t('This field stores user choices about some dates in the database.'),
      'default_widget' => 'makemeeting_choices',
      'default_formatter' => 'makemeeting_answers',
    ),
  );
}

/**
 * Implements hook_field_widget_info().
 */
function makemeeting_field_widget_info() {
  return array(
    'makemeeting_choices' => array(
      'label' => t('Makemeeting choices'),
      'field types' => array(
        'makemeeting',
      ),
      'behaviors' => array(
        'default value' => FIELD_BEHAVIOR_NONE,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_settings_form().
 */
function makemeeting_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  $form = array();
  if ($widget['type'] == 'makemeeting_choices') {
    $form['size'] = array(
      '#type' => 'textfield',
      '#title' => t('Number of dates'),
      '#default_value' => isset($settings['size']) ? $settings['size'] : 1,
      '#element_validate' => array(
        'element_validate_integer',
      ),
      '#required' => TRUE,
      '#size' => 5,
      '#description' => t('Default number of date choices to display. Must be positive or equal to 0.'),
    );
  }
  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function makemeeting_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element += array(
    '#type' => 'fieldset',
  );
  $item =& $items[$delta];

  // Simple options form elements about the poll
  $element['hidden'] = array(
    '#title' => t('Hidden poll'),
    '#description' => t('Confidential participation: only you and administrators can see the answers.'),
    '#type' => 'checkbox',
    '#default_value' => isset($item['hidden']) ? $item['hidden'] : '',
  );
  $element['one_option'] = array(
    '#title' => t('Participant can only choose one option'),
    '#description' => t('By default all options are selectable. This setting limits the choice to one option per participant.'),
    '#type' => 'checkbox',
    '#default_value' => isset($item['one_option']) ? $item['one_option'] : '',
  );
  $element['limit'] = array(
    '#title' => t('Limit the number of participants per option'),
    '#description' => t('Poll as registration form: As soon as the indicated limit has been reached, the respective option is no longer available. 0 for unlimited'),
    '#type' => 'textfield',
    '#default_value' => isset($item['limit']) ? $item['limit'] : 0,
    '#element_validate' => array(
      'element_validate_integer',
    ),
  );
  $element['yesnomaybe'] = array(
    '#title' => t('Provide a "Maybe" option'),
    '#description' => t('Provide a third "Maybe" option in case users may be available.'),
    '#type' => 'checkbox',
    '#default_value' => isset($item['yesnomaybe']) ? $item['yesnomaybe'] : '',
    // Hide if one_option is checked
    '#states' => array(
      'visible' => array(
        ':input[name$="[one_option]"]' => array(
          'checked' => FALSE,
        ),
      ),
    ),
  );
  $element['timezone'] = array(
    '#title' => t('Timezone'),
    '#description' => t('Set this to your timezone so that dates can be converted for users in other parts of the world.'),
    '#type' => 'select',
    '#default_value' => isset($item['timezone']) ? $item['timezone'] : drupal_get_user_timezone(),
    '#options' => system_time_zones(),
  );
  $element['closed'] = array(
    '#title' => t('Closed'),
    '#description' => t('Check this when you want to close the poll.'),
    '#type' => 'checkbox',
    '#default_value' => isset($item['closed']) ? $item['closed'] : '',
  );

  // Now let's take care of the choices part
  // $process is a callback that we will attach to the wrapper in order
  // to add parents to the form elements. This is so we can store
  // nested values in the DB table with field storage API.
  $fieldset_info = element_info('fieldset');
  $process = array_merge($fieldset_info['#process'], array(
    '_makemeeting_rebuild_parents',
  ));
  $element['choices_wrapper'] = array(
    '#tree' => FALSE,
    '#prefix' => '<div class="clearfix" id="makemeeting-choices-wrapper">',
    '#suffix' => '</div>',
    '#process' => $process,
  );

  // Container just for the poll choices.
  $element['choices_wrapper']['choices'] = array(
    '#prefix' => '<div id="makemeeting-choices">',
    '#suffix' => '</div>',
    '#theme' => 'makemeeting_choices',
  );

  // Fill the item values with previously submitted values
  if (!empty($form_state['values'])) {
    $submitted_values = drupal_array_get_nested_value($form_state['values'], $element['#field_parents']);
    $submitted_values = $submitted_values[$element['#field_name']][$element['#language']][$element['#delta']];
    if (isset($submitted_values['choices'])) {
      $item['choices'] = $submitted_values['choices'];
    }
  }

  // Calculate the suggestion count
  if (empty($form_state['suggestion_count'])) {
    $form_state['suggestion_count'] = 1;
  }
  if (isset($item['choices'])) {
    foreach ($item['choices'] as $choice) {
      if ($form_state['suggestion_count'] < count($choice['chsuggestions'])) {
        $form_state['suggestion_count'] = count($choice['chsuggestions']);
      }
    }
  }

  // Determine choice count if not provided
  if (!isset($form_state['choice_count'])) {
    try {
      list($entity_id) = entity_extract_ids($element['#entity_type'], $element['#entity']);
    } catch (Exception $e) {

      // Fail silently if the field is not attached to any entity yet.
    }

    // If the entity isn't created yet, offer the default number of choices,
    // else the number of previous choices, or 0 if none
    if (!isset($entity_id)) {
      $widget = $instance['widget'];
      $settings = $widget['settings'];
      $form_state['choice_count'] = isset($settings['size']) ? $settings['size'] : 1;
    }
    else {
      $form_state['choice_count'] = isset($item['choices']) ? count($item['choices']) : 0;
    }
  }

  // Add the current choices to the form.
  $delta = 0;
  if (isset($item['choices']) && $form_state['choice_count']) {

    // Sort choices by their dates
    asort($item['choices']);
    $delta = count($item['choices']);
    foreach ($item['choices'] as $key => $choice) {
      $element['choices_wrapper']['choices'][$key] = _makemeeting_choice_form($key, $choice['chdate'], $choice['chsuggestions'], $form_state['suggestion_count']);
      if ($form_state['choice_count'] == 1) {
        $element['choices_wrapper']['choices'][$key]['chremove']['#access'] = FALSE;
      }
    }
  }

  // Add choices as required.
  if ($form_state['choice_count']) {

    // Add a day to the last option's timestamp for the new default date
    $date = isset($choice['chdate']) ? _makemeeting_date_timestamp($choice['chdate']) : new DateTime();
    $existing_delta = $delta;
    for (; $delta < $form_state['choice_count']; $delta++) {
      $date
        ->add(new DateInterval('P1D'));

      // Build default date
      $default_date = array(
        'month' => $date
          ->format('n'),
        'day' => $date
          ->format('j'),
        'year' => $date
          ->format('Y'),
      );
      $key = 'new:' . ($delta - $existing_delta);
      $element['choices_wrapper']['choices'][$key] = _makemeeting_choice_form($key, $default_date, NULL, $form_state['suggestion_count']);
      if ($form_state['choice_count'] == 1) {
        $element['choices_wrapper']['choices'][$key]['chremove']['#access'] = FALSE;
      }
    }
  }

  // We prefix our buttons with 'makemeeting' to avoid conflicts
  // with other modules using Ajax-enabled buttons with the id 'more'.
  $default_submit = array(
    '#type' => 'submit',
    '#limit_validation_errors' => array(
      array(
        'choices',
      ),
    ),
    '#submit' => array(
      'makemeeting_choices_submit',
    ),
    '#ajax' => array(
      'callback' => 'makemeeting_choice_js',
      'wrapper' => 'makemeeting-choices-wrapper',
      'effect' => 'fade',
    ),
  );
  $element['choices_wrapper']['makemeeting_more_choices'] = $default_submit + array(
    '#value' => t('More choices'),
    '#attributes' => array(
      'title' => t("If the amount of rows above isn't enough, click here to add more choices."),
    ),
    '#weight' => 1,
  );
  $element['choices_wrapper']['makemeeting_more_suggestions'] = $default_submit + array(
    '#value' => t('More suggestions'),
    '#attributes' => array(
      'title' => t("If the amount of boxes above isn't enough, click here to add more suggestions."),
    ),
    '#weight' => 2,
  );
  if ($form_state['choice_count'] > 1) {
    $element['choices_wrapper']['makemeeting_copy_suggestions'] = $default_submit + array(
      '#value' => t('Copy and paste first row'),
      '#attributes' => array(
        'title' => t("Copy the suggestions from the first to the other rows."),
      ),
      '#weight' => 3,
    );
  }
  return $element;
}

/**
 * Helper function to construct a row of the widget form
 *
 * @param $key
 * @param string $value
 * @param array $suggestions
 * @param int $size
 * @return array
 */
function _makemeeting_choice_form($key, $value = '', $suggestions = array(), $size = 1) {
  $form = array(
    '#tree' => TRUE,
  );

  // We'll manually set the #parents property of these fields so that
  // their values appear in the $form_state['values']['choices'] array
  // They are to be updated later in #process as we don't know here the
  // hierarchy of the parents
  $form['chdate'] = array(
    '#type' => 'date',
    '#title' => t('Date'),
    '#title_display' => 'invisible',
    '#default_value' => $value,
    '#parents' => array(
      'choices',
      $key,
      'chdate',
    ),
  );
  $form['chremove'] = array(
    '#type' => 'submit',
    '#parents' => array(),
    '#submit' => array(
      'makemeeting_choices_submit',
    ),
    '#limit_validation_errors' => array(
      array(
        'choices',
      ),
    ),
    '#ajax' => array(
      'callback' => 'makemeeting_choice_js',
      'wrapper' => 'makemeeting-choices-wrapper',
      'effect' => 'fade',
    ),
    '#value' => t('Remove'),
    // Assigning key as name to make each 'Remove' button unique
    '#name' => $key,
  );
  $form['chsuggestions'] = array(
    '#tree' => TRUE,
    '#parents' => array(
      'choices',
      $key,
      'chsuggestions',
    ),
  );
  for ($i = 0; $i < $size; $i++) {
    $key2 = 'sugg:' . $i;
    $form['chsuggestions'][$key2] = array(
      '#type' => 'textfield',
      '#title_display' => 'invisible',
      '#default_value' => isset($suggestions[$key2]) ? $suggestions[$key2] : '',
      '#size' => 5,
      '#maxlength' => 255,
      '#parents' => array(
        'choices',
        $key,
        'chsuggestions',
        $key2,
      ),
    );
  }
  return $form;
}

/**
 * Helper function to update the form elements parents. This is required
 * for the element to be saved properly by the Field API (see #process)
 *
 * @param $element
 * @return mixed
 */
function _makemeeting_rebuild_parents(&$element) {
  $parents = array_diff($element['#array_parents'], $element['#parents']);
  $elements = array(
    'makemeeting_more_choices',
    'makemeeting_more_suggestions',
    'makemeeting_copy_suggestions',
  );
  foreach ($elements as $e) {
    $element[$e]['#parents'] = $parents;
    if (isset($element[$e]['#limit_validation_errors'])) {
      $element[$e]['#limit_validation_errors'][0] = array_merge($parents, $element[$e]['#limit_validation_errors'][0]);
    }
  }
  $element['makemeeting_more_suggestions']['#parents'] = $parents;
  $element['makemeeting_copy_suggestions']['#parents'] = $parents;
  foreach (element_children($element['choices']) as $key1) {
    foreach (element_children($element['choices'][$key1]) as $key2) {
      $keys = element_children($element['choices'][$key1][$key2]);
      if (empty($keys)) {
        $element['choices'][$key1][$key2]['#parents'] = array_merge($parents, $element['choices'][$key1][$key2]['#parents']);
      }
      else {
        foreach ($keys as $key3) {
          $element['choices'][$key1][$key2][$key3]['#parents'] = array_merge($parents, $element['choices'][$key1][$key2][$key3]['#parents']);
        }
      }
    }
  }
  return $element;
}

/**
 * Implements hook_field_validate().
 */
function makemeeting_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  foreach ($items as $delta => $item) {
    if (isset($item['choices'])) {

      // Verify that we do not have duplicate dates
      $dates = array();
      foreach ($item['choices'] as $chid => $chvalue) {
        $date = implode('', $chvalue['chdate']);
        if (in_array($date, $dates)) {

          // Register an error with the choice id
          $errors[$field['field_name']][$langcode][$delta][] = array(
            'error' => 'duplicate_dates',
            'message' => t('%name: you can\'t have duplicate dates.', array(
              '%name' => $instance['label'],
            )),
            'chid' => $chid,
          );
        }
        else {
          $dates[] = $date;
        }
      }
    }
  }
}

/**
 * Implements hook_field_widget_error().
 */
function makemeeting_field_widget_error($element, $error) {

  // Show our error concerning duplicate dates here
  $choices = $element['choices_wrapper']['choices'];
  form_error($choices[$error['chid']]['chdate'], $error['message']);
}

/**
 * Implements hook_field_is_empty().
 */
function makemeeting_field_is_empty($item) {
  return empty($item['choices']);
}

/**
 * Implements hook_field_formatter_info().
 */
function makemeeting_field_formatter_info() {
  return array(
    'makemeeting_answers' => array(
      'label' => t('Makemeeting answers'),
      'field types' => array(
        'makemeeting',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 *
 * Build a renderable array for a field value.
 *
 * @param $entity_type
 *   The type of $entity.
 * @param $entity
 *   The entity being displayed.
 * @param $field
 *   The field structure.
 * @param $instance
 *   The field instance.
 * @param $langcode
 *   The language associated with $items.
 * @param $items
 *   Array of values for this field.
 * @param $display
 *   The display settings to use, as found in the 'display' entry of instance
 *   definitions. The array notably contains the following keys and values;
 *   - type: The name of the formatter to use.
 *   - settings: The array of formatter settings.
 *
 * @return array
 *  A renderable array for the $items, as an array of child elements keyed
 */
function makemeeting_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $content = array();
  foreach ($items as $delta => $item) {
    list($entity_id) = entity_extract_ids($entity_type, $entity);
    $instance += array(
      'entity_id' => $entity_id,
      'language' => $langcode,
      'delta' => $delta,
    );

    // Define answers hidden status here as we have $field variable available
    $item['is_hidden'] = (bool) $item['hidden'];
    if ($item['is_hidden']) {
      $access = field_access('edit', $field, $entity_type, $entity);
      if ($entity_type == 'node') {
        $access = $access && node_access('update', $entity);
      }
      elseif (module_exists('entity')) {
        $access = $access && entity_access('edit', $entity_type, $entity);
      }
      $item['is_hidden'] = $item['is_hidden'] && !$access;
    }
    if (isset($instance['entity_id'])) {
      $content[] = drupal_get_form('makemeeting_answers_form_' . $instance['entity_id'], $item, $instance);
    }
  }
  return $content;
}

/**
 * Implements hook_field_load().
 */
function makemeeting_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {

  // Unserialize array values
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
      if ($field['type'] == 'makemeeting' && is_string($items[$id][$delta]['choices'])) {
        $items[$id][$delta]['choices'] = unserialize($items[$id][$delta]['choices']);
      }
    }
  }
}

/**
 * Implements hook_field_presave().
 */
function makemeeting_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  if ($field['type'] == 'makemeeting') {
    foreach ($items as $delta => $item) {
      if (isset($item['choices'])) {

        // Sort the choices by date
        $choices = array_values($item['choices']);
        usort($choices, '_makemeeting_sort_choices');

        // Affect timestamp as keys
        $timestamped_choices = array();
        foreach ($choices as $choice) {
          $dateTime = _makemeeting_date_timestamp($choice['chdate']);
          $timestamped_choices[$dateTime
            ->format('d-m-Y')] = $choice;
        }

        // Serialize the whole array
        $items[$delta]['choices'] = serialize($timestamped_choices);
      }
    }
  }
}

/**
 * Helper function to sort choice dates
 */
function _makemeeting_sort_choices($a, $b) {
  if ($a['chdate']['year'] == $b['chdate']['year']) {
    if ($a['chdate']['month'] == $b['chdate']['month']) {
      if ($a['chdate']['day'] == $b['chdate']['day']) {
        return 0;
      }
      else {
        return (int) $a['chdate']['day'] < (int) $b['chdate']['day'] ? -1 : 1;
      }
    }
    else {
      return (int) $a['chdate']['month'] < (int) $b['chdate']['month'] ? -1 : 1;
    }
  }
  else {
    return (int) $a['chdate']['year'] < (int) $b['chdate']['year'] ? -1 : 1;
  }
}

/**
 * Submit handler to add more choices or suggestions to a poll form,
 * or to copy the first row of suggestions to the others
 *
 * This handler is run regardless of whether JS is enabled or not. It makes
 * changes to the form state. If the button was clicked with JS disabled, then
 * the page is reloaded with the complete rebuilt form. If the button was
 * clicked with JS enabled, then ajax_form_callback() calls makemeeting_choice_js() to
 * return just the changed part of the form.
 */
function makemeeting_choices_submit($form, &$form_state) {
  $clicked_button = $form_state['clicked_button'];

  // Get the element
  $element =& drupal_array_get_nested_value($form_state['input'], $clicked_button['#parents']);

  // If clicked button is 'Remove', unset corresponding choice
  if ($clicked_button['#value'] == t('Remove')) {
    unset($element['choices'][$clicked_button['#name']]);
    $form_state['choice_count'] = count($element['choices']);
  }

  // Affect timestamp as key to each choice
  $choices = array();
  if (isset($element['choices'])) {
    foreach ($element['choices'] as $choice) {
      $dateTime = _makemeeting_date_timestamp($choice['chdate']);
      $choices['choices'][$dateTime
        ->format('d-m-Y')] = $choice;
    }
  }

  // Handle other operations
  if (!empty($form_state['values']['op'])) {
    switch ($form_state['values']['op']) {

      // Add 1 more choice to the form.
      case t('More choices'):

        // Increment choice count
        $form_state['choice_count'] = count($element['choices']) + 1;
        break;

      // Add 1 more suggestion to the form.
      case t('More suggestions'):
        $form_state['suggestion_count']++;
        break;

      // Copy first row suggestions to the other rows.
      case t('Copy and paste first row'):
        $first = reset($choices['choices']);
        foreach (array_keys($choices['choices']) as $key) {
          $choices['choices'][$key]['chsuggestions'] = $first['chsuggestions'];
        }
        break;
    }
  }

  // Replace submitted values by our own work
  drupal_array_set_nested_value($form_state['input'], $clicked_button['#parents'], $choices);
  drupal_array_set_nested_value($form_state['values'], $clicked_button['#parents'], $choices);

  // Require the widget form to rebuild the choices
  // with the values in $form_state
  $form_state['rebuild'] = TRUE;
}

/**
 * Ajax callback in response to new choices being added to the form.
 *
 * This returns the new page content to replace the page content made obsolete
 * by the form submission.
 */
function makemeeting_choice_js($form, $form_state) {

  // Get the element from the parents of the clicked button
  $element = drupal_array_get_nested_value($form, $form_state['clicked_button']['#parents']);
  return $element['choices_wrapper'];
}

Functions

Namesort descending Description
makemeeting_choices_submit Submit handler to add more choices or suggestions to a poll form, or to copy the first row of suggestions to the others
makemeeting_choice_js Ajax callback in response to new choices being added to the form.
makemeeting_field_formatter_info Implements hook_field_formatter_info().
makemeeting_field_formatter_view Implements hook_field_formatter_view().
makemeeting_field_info Implements hook_field_info().
makemeeting_field_is_empty Implements hook_field_is_empty().
makemeeting_field_load Implements hook_field_load().
makemeeting_field_presave Implements hook_field_presave().
makemeeting_field_validate Implements hook_field_validate().
makemeeting_field_widget_error Implements hook_field_widget_error().
makemeeting_field_widget_form Implements hook_field_widget_form().
makemeeting_field_widget_info Implements hook_field_widget_info().
makemeeting_field_widget_settings_form Implements hook_field_widget_settings_form().
_makemeeting_choice_form Helper function to construct a row of the widget form
_makemeeting_rebuild_parents Helper function to update the form elements parents. This is required for the element to be saved properly by the Field API (see #process)
_makemeeting_sort_choices Helper function to sort choice dates