msnf.module in Multistep Nodeform 7
Same filename and directory in other branches
Main functions for module "Multistep Nodeform".
File
msnf.moduleView source
<?php
/**
* @file
* Main functions for module "Multistep Nodeform".
*/
/**
* Implements hook_menu().
*/
function msnf_menu() {
$items = array();
// Ensure the following is not executed until field_bundles is working and
// tables are updated. Needed to avoid errors on initial installation.
if (defined('MAINTENANCE_MODE')) {
return $items;
}
// Create tabs for all possible bundles.
foreach (entity_get_info() as $entity_type => $entity_info) {
if (isset($entity_info['fieldable']) && $entity_info['fieldable']) {
foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
if (isset($bundle_info['admin'])) {
// Extract path information from the bundle.
$path = $bundle_info['admin']['path'];
// Extract the argument position of the bundle name string
// ('bundle argument') and pass that position to the menu loader. The
// position needs to be casted into a string; otherwise it would be
// replaced with the bundle name string.
if (isset($bundle_info['admin']['bundle argument'])) {
$bundle_arg = $bundle_info['admin']['bundle argument'];
$bundle_pos = (string) $bundle_arg;
}
else {
$bundle_arg = $bundle_name;
$bundle_pos = '0';
}
// This is the position of the %msnf_step_menu placeholder in the
// items below.
$argument_position = count(explode('/', $path)) + 1;
// Extract access information, providing defaults.
$access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array(
'access callback',
'access arguments',
)));
$access += array(
'access callback' => 'user_access',
'access arguments' => array(
'administer site configuration',
),
);
$items["{$path}/steps/%msnf_step_menu/delete"] = array(
'load arguments' => array(
$entity_type,
$bundle_arg,
$bundle_pos,
'%map',
),
'title' => 'Delete',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'msnf_delete_form',
$argument_position,
),
'type' => MENU_CALLBACK,
'file' => 'includes/msnf.field_ui.inc',
) + $access;
}
}
}
}
return $items;
}
/**
* Implements hook_permission().
*/
function msnf_permission() {
return array(
'administer form steps' => array(
'title' => t('Administer form steps'),
'description' => t('Display the administration for form steps.'),
),
);
}
/**
* Implements hook_block_info().
*/
function msnf_block_info() {
$blocks = array();
$blocks['msnf_step_data'] = array(
'info' => t('Multistep Nodeform: Step information'),
'cache' => DRUPAL_NO_CACHE,
);
return $blocks;
}
/**
* Implements hook_block_view().
*/
function msnf_block_view($delta = '') {
$data = drupal_static('msnf_step_data');
if (!empty($data)) {
$block = array();
$block['subject'] = t('Form steps');
$block['content'] = array(
'#theme' => 'msnf_block_step_info',
'#steps' => $data['steps'],
'#current_step' => $data['current_step'],
'#form_state' => $data['form_state'],
// Attach styling.
'#attached' => array(
'css' => array(
drupal_get_path('module', 'msnf') . '/theme/msnf.steps.css',
),
),
);
return $block;
}
}
/**
* Menu Wildcard loader function to load step definitions.
*
* @param <string> $step_name
* The name of the step, as contained in the path.
* @param <string> $entity_type
* The name of the entity.
* @param <string> $bundle_name
* The name of the bundle, as contained in the path.
* @param <int> $bundle_pos
* The position of $bundle_name in $map.
* @param <array> $map
* The translated menu router path argument map.
*/
function msnf_step_menu_load($step_name, $entity_type, $bundle_name, $bundle_pos, $map) {
if ($bundle_pos > 0) {
$bundle = $map[$bundle_pos];
$bundle_name = field_extract_bundle($entity_type, $bundle);
}
return msnf_load_step($step_name, $entity_type, $bundle_name);
}
/**
* Loads a step definition.
*
* @param <string> $step_name
* The name of the form step.
* @param <string> $entity_type
* The name of the entity.
* @param <string> $bundle_name
* The name of the bundle.
*/
function msnf_load_step($step_name, $entity_type, $bundle_name) {
ctools_include('export');
$objects = ctools_export_load_object('msnf_step', 'conditions', array(
'step_name' => $step_name,
'entity_type' => $entity_type,
'bundle' => $bundle_name,
));
$object = array_shift($objects);
if ($object && isset($object->data)) {
return msnf_unpack($object);
}
return $object;
}
/**
* Implements hook_ctools_plugin_api().
*/
function msnf_ctools_plugin_api($owner, $api) {
if ($owner == 'msnf' && $api == 'msnf') {
return array(
'version' => 1,
);
}
}
/**
* Implements hook_theme().
*/
function msnf_theme() {
return array(
'msnf_form_step' => array(
'render element' => 'element',
),
'msnf_block_step_info' => array(
'variables' => array(
'steps' => array(),
'current_step' => NULL,
'form_state' => array(),
),
'path' => drupal_get_path('module', 'msnf') . '/theme',
'template' => 'msnf-block-step-info',
),
'msnf_form_step_info' => array(
'variables' => array(
'steps' => array(),
'current_step' => NULL,
),
'path' => drupal_get_path('module', 'msnf') . '/theme',
'template' => 'msnf-form-step-info',
),
);
}
/**
* Implements hook_field_attach_delete_bundle().
*
* @param <string> $entity_type
* Type of entity.
* @param <string> $bundle
* Bundle name.
*/
function msnf_field_attach_delete_bundle($entity_type, $bundle) {
ctools_include('export');
$list = msnf_read_steps(array(
'bundle' => $bundle,
'entity_type' => $entity_type,
));
// Delete the entity's entry from msnf_step of all entities.
// We fetch the form steps first to assign the removal task to ctools.
if (isset($list[$entity_type], $list[$entity_type][$bundle])) {
foreach ($list[$entity_type][$bundle] as $group) {
ctools_export_crud_delete('msnf_step', $group);
}
}
}
/**
* Implements hook_field_extra_fields().
*/
function msnf_field_extra_fields() {
$extra = array();
foreach (node_type_get_types() as $type) {
$step_info = msnf_info_steps('node', $type->type);
if (empty($step_info)) {
// There are no steps for this type so skip to next.
continue;
}
$extra['node'][$type->type]['form'] = array(
'additional_settings' => array(
'label' => t('Additional Information'),
'description' => t('Node module element'),
'weight' => 100,
),
);
}
return $extra;
}
/**
* Implements hook_field_attach_form().
*/
function msnf_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
// Load steps for this form.
msnf_attach_steps($form, $form_state);
if (empty($form['#steps'])) {
// Nothing to do here.
return;
}
// Init first step if no step is set before.
if (!isset($form_state['storage']['step'])) {
$step_names = array_keys($form['#steps']);
$form_state['storage']['step'] = $step_names[0];
}
// If one specific step should be displayed ...
if (isset($_GET['step'])) {
$step_requested = filter_xss_admin($_GET['step']);
_msnf_form_step_set_current($step_requested, $form, $form_state);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function msnf_form_field_ui_field_overview_form_alter(&$form, &$form_state, $form_id) {
form_load_include($form_state, 'inc', 'msnf', 'includes/msnf.field_ui');
msnf_field_ui_overview_form_alter($form, $form_state);
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function msnf_form_node_form_alter(&$form, &$form_state, $form_id) {
if (isset($form['#steps'])) {
// Init static step data.
$step_data =& drupal_static('msnf_step_data');
$step_data = array(
'steps' => $form['#steps'],
'current_step' => _msnf_form_step_get_current($form, $form_state),
'form_state' => $form_state,
);
}
if (empty($form['#steps'])) {
// No need for further modifications.
return;
}
// Attach styling.
$form['#attached']['css'][] = drupal_get_path('module', 'msnf') . '/theme/msnf.field_ui.css';
// Add step buttons to form.
_msnf_form_attach_buttons($form, $form_state);
// Hide all elements that do not belong to the current step.
_msnf_hide_fields($form, $form_state);
// Restore field values.
_msnf_restore_values($form, $form_state);
// Add a custom validation handler to rebuild the form.
$form['#validate'][] = 'msnf_entity_form_validate';
}
/**
* Implements hook_msnf_formatter_info().
*/
function msnf_msnf_formatter_info() {
return array(
'default' => array(
'label' => t('Default'),
'description' => t('The form step renders the containing form elements in a single div with a title and description.'),
'instance_settings' => array(
'description' => '',
'show_label' => 1,
'label_element' => 'h3',
'classes' => '',
'skip_non_required' => 1,
'hide_if_empty' => 0,
'buttons' => array(
'previous' => t('Back'),
'next' => t('Next'),
'skip' => t('Skip next step'),
),
),
),
);
}
/**
* Implements hook_msnf_format_settings().
* If the step has no format settings, default ones will be added.
*
* @params <object> $step
* The step object.
*
* @return <array> $form
* The form element for the format settings.
*/
function msnf_msnf_format_settings($step) {
// Add a wrapper for extra settings to use by others.
$form = array(
'instance_settings' => array(
'#tree' => TRUE,
'#weight' => 2,
),
);
$step_types = msnf_formatter_info();
$formatter = $step_types[$step->format_type];
$form['instance_settings']['skip_non_required'] = array(
'#type' => 'checkbox',
'#title' => t('Allow skipping this step if it contains no required fields.'),
'#default_value' => isset($step->format_settings['instance_settings']['skip_non_required']) ? $step->format_settings['instance_settings']['skip_non_required'] : (isset($formatter['instance_settings']['skip_non_required']) ? $formatter['instance_settings']['skip_non_required'] : ''),
'#weight' => 1,
);
$form['instance_settings']['hide_if_empty'] = array(
'#type' => 'checkbox',
'#title' => t('Hide the entire step if it contains no fields or none of the fields is accessible by the user.'),
'#default_value' => isset($step->format_settings['instance_settings']['hide_if_empty']) ? $step->format_settings['instance_settings']['hide_if_empty'] : (isset($formatter['instance_settings']['hide_if_empty']) ? $formatter['instance_settings']['hide_if_empty'] : ''),
'#weight' => 1,
);
if (isset($formatter['instance_settings']['classes'])) {
$form['instance_settings']['classes'] = array(
'#title' => t('Extra CSS classes'),
'#type' => 'textfield',
'#default_value' => isset($step->format_settings['instance_settings']['classes']) ? $step->format_settings['instance_settings']['classes'] : (isset($formatter['instance_settings']['classes']) ? $formatter['instance_settings']['classes'] : ''),
'#weight' => 3,
'#element_validate' => array(
'msnf_validate_css_class',
),
);
}
if (isset($formatter['instance_settings']['description'])) {
$form['instance_settings']['description'] = array(
'#title' => t('Step description'),
'#type' => 'textarea',
'#rows' => 3,
'#default_value' => isset($step->format_settings['instance_settings']['description']) ? $step->format_settings['instance_settings']['description'] : (isset($formatter['instance_settings']['description']) ? $formatter['instance_settings']['description'] : ''),
'#weight' => 0,
);
}
// Add optional instance_settings.
switch ($step->format_type) {
case 'default':
$form['instance_settings']['show_label'] = array(
'#title' => t('Show label'),
'#type' => 'select',
'#options' => array(
0 => t('No'),
1 => t('Yes'),
),
'#default_value' => isset($step->format_settings['instance_settings']['show_label']) ? $step->format_settings['instance_settings']['show_label'] : $formatter['instance_settings']['show_label'],
'#weight' => 2,
);
$form['instance_settings']['label_element'] = array(
'#title' => t('Label element'),
'#type' => 'select',
'#options' => array(
'h2' => t('Header 2'),
'h3' => t('Header 3'),
),
'#default_value' => isset($step->format_settings['instance_settings']['label_element']) ? $step->format_settings['instance_settings']['label_element'] : $formatter['instance_settings']['label_element'],
'#weight' => 2,
);
$form['instance_settings']['buttons'] = array(
'#title' => t('Button labels'),
'#type' => 'fieldset',
'#tree' => TRUE,
'#weight' => 3,
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['instance_settings']['buttons']['previous'] = array(
'#title' => t('Back'),
'#type' => 'textfield',
'#description' => t('Text to use as label for back-button. Leave empty to use the title of the previous step.'),
'#default_value' => isset($step->format_settings['instance_settings']['buttons']['previous']) ? $step->format_settings['instance_settings']['buttons']['previous'] : (isset($formatter['instance_settings']['buttons']['previous']) ? $formatter['instance_settings']['buttons']['previous'] : ''),
'#weight' => 0,
);
$form['instance_settings']['buttons']['next'] = array(
'#title' => t('Next'),
'#type' => 'textfield',
'#description' => t('Text to use as label for next-button. Leave empty to use the title of the next step.'),
'#default_value' => isset($step->format_settings['instance_settings']['buttons']['next']) ? $step->format_settings['instance_settings']['buttons']['next'] : (isset($formatter['instance_settings']['buttons']['next']) ? $formatter['instance_settings']['buttons']['next'] : ''),
'#weight' => 1,
);
$form['instance_settings']['buttons']['skip'] = array(
'#title' => t('Skip'),
'#type' => 'textfield',
'#description' => t('Text to use as label for skip-button.'),
'#default_value' => isset($step->format_settings['instance_settings']['buttons']['skip']) ? $step->format_settings['instance_settings']['buttons']['skip'] : (isset($formatter['instance_settings']['buttons']['skip']) ? $formatter['instance_settings']['buttons']['skip'] : ''),
'#weight' => 2,
);
break;
default:
}
return $form;
}
/**
* Helper function to prepare basic variables needed for most formatters.
*/
function msnf_pre_render_prepare(&$step) {
// Prepare extra classes.
$step->classes = array(
'step-' . $step->format_type,
str_replace('_', '-', $step->step_name),
);
if (isset($step->format_settings['instance_settings']) && !empty($step->format_settings['instance_settings']['skip_non_required'])) {
$step->classes[] = 'skippable';
}
$step->classes = implode(' ', $step->classes);
if (isset($step->format_settings['instance_settings'], $step->format_settings['instance_settings']['classes'])) {
$step->classes .= ' ' . check_plain($step->format_settings['instance_settings']['classes']);
}
$step->description = isset($step->format_settings['instance_settings']['description']) ? filter_xss_admin($step->format_settings['instance_settings']['description']) : '';
}
/**
* Implements hook_msnf_step_pre_render().
*
* @param <array> $elements
* Array of elements to render.
* @param <object> $step
* The step info.
* @param <array> $form
* The form where the element needs to be rendered.
*/
function msnf_msnf_step_pre_render(&$element, &$step, &$form) {
// Prepare step rendering.
msnf_pre_render_prepare($step);
$element['#id'] = $form['#entity_type'] . '_' . $form['#bundle'] . '_' . $step->step_name;
$element['#weight'] = $step->weight;
// Call the pre render function for the format type.
$function = "msnf_step_pre_render_" . str_replace("-", "_", $step->format_type);
if (function_exists($function)) {
$function($element, $step, $form);
}
}
/**
* Implements msnf_step_pre_render_<format-type>.
* Format type: 'default'.
*
* @param <array> $element
* The step form element.
* @param <object> $step
* The step object prepared for pre_render.
* @param <string> $form
* The root element or form.
*/
function msnf_step_pre_render_default(&$element, $step, &$form) {
$element['msnf_form_step_info'] = array(
'#theme' => 'msnf_form_step_info',
'#steps' => $form['#steps'],
'#current_step' => $step,
'#weight' => -100,
);
}
/**
* Implements hook_msnf_format_summary().
*/
function msnf_msnf_format_summary($step) {
$step_form = module_invoke_all('msnf_format_settings', $step);
$output = '';
if (isset($step->format_settings['instance_settings'])) {
$last = end($step->format_settings['instance_settings']);
foreach ($step->format_settings['instance_settings'] as $key => $value) {
if (!is_numeric($value) && empty($value)) {
continue;
}
$output .= '<strong>' . $key . '</strong> ';
if (isset($step_form['instance_settings'], $step_form['instance_settings'][$key]['#options'])) {
$value = $step_form['instance_settings'][$key]['#options'][$value];
}
if (is_array($value)) {
$output .= '<br />';
$last_value = end($value);
foreach ($value as $vkey => $vvalue) {
$output .= ' <strong>' . $vkey . '</strong> ';
// Shorten the string.
if (drupal_strlen($vvalue) > 38) {
$vvalue = truncate_utf8($vvalue, 50, TRUE, TRUE);
}
elseif (is_numeric($value)) {
$vvalue = $vvalue == '1' ? t('yes') : t('no');
}
$output .= check_plain($vvalue);
$output .= $last_value == $vvalue ? ' ' : '<br />';
}
continue;
}
// Shorten the string.
if (drupal_strlen($value) > 38) {
$value = truncate_utf8($value, 50, TRUE, TRUE);
}
elseif (is_numeric($value)) {
$value = $value == '1' ? t('yes') : t('no');
}
$output .= check_plain($value);
$output .= $last == $value ? ' ' : '<br />';
}
}
return $output;
}
/**
* Implements hook_element_info().
*/
function msnf_element_info() {
$types['msnf_form_step'] = array(
'#theme_wrappers' => array(
'msnf_form_step',
),
'#default_tab' => '',
'#process' => array(
'msnf_form_process_msnf_form_step',
),
);
return $types;
}
/**
* Implements hook_field_attach_rename_bundle().
*/
function msnf_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
db_query('UPDATE {msnf_step} SET bundle = :bundle WHERE bundle = :old_bundle AND entity_type = :entity_type', array(
':bundle' => $bundle_new,
':old_bundle' => $bundle_old,
':entity_type' => $entity_type,
));
}
/**
* Creates a single form step.
*
* @param <array> $element
* An associative array containing the properties and children of the step.
* @param <array> $form_state
* The $form_state array for the form this widget belongs to.
*
* @return <array>
* The processed element.
*/
function msnf_form_process_msnf_form_step($element, &$form_state) {
// Inject a new element as child.
$element['step'] = array(
'#type' => 'markup',
'#theme_wrappers' => array(),
'#parents' => $element['#parents'],
);
return $element;
}
/**
* Returns HTML for a single form step.
*
* @param <array> $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the form included in the step. Properties used: #children.
*
* @ingroup themeable
*/
function theme_msnf_form_step($variables) {
$element = $variables['element'];
$output = $element['#children'];
return $output;
}
/**
* Define variables used in msnf-block-step-info.tpl.php.
*
* @param <array> $vars
* Variables available for use in msnf-block-step-info.tpl.php.
*
* @see msnf-block-step-info.tpl.php.
*/
function msnf_preprocess_msnf_block_step_info(&$vars) {
$items = array();
foreach ($vars['steps'] as $step_name => $step) {
$item = array(
'data' => msnf_translate(array(
$step->step_name,
$step->bundle,
'label',
), $step->label),
'title' => msnf_translate(array(
$step->step_name,
$step->bundle,
'description',
), $step->format_settings['instance_settings']['description']),
'class' => array(
'msnf-form-step',
),
);
if ($step->identifier == $vars['current_step']->identifier) {
// Add "active" class for current step.
$item['class'][] = 'active';
}
$items[] = $item;
}
$vars['steps_rendered'] = theme('item_list', array(
'items' => $items,
'attributes' => array(
'class' => array(
'msnf-forms-steps',
),
),
));
}
/**
* Define variables used in msnf-form-step-info.tpl.php.
*/
function msnf_preprocess_msnf_form_step_info(&$vars) {
$vars['step_classes_array'] = array();
$vars['step_classes_array'][] = 'step-format';
// Get step settings.
$vars['show_step_title'] = isset($vars['current_step']->format_settings['instance_settings']['show_label']) ? $vars['current_step']->format_settings['instance_settings']['show_label'] : 0;
$vars['title_element'] = isset($vars['current_step']->format_settings['instance_settings']['label_element']) ? $vars['current_step']->format_settings['instance_settings']['label_element'] : 'h2';
// Add classes defined in step configuration.
$vars['step_classes_array'][] = filter_xss($vars['current_step']->classes);
$vars['step_title'] = msnf_translate(array(
$vars['current_step']->step_name,
$vars['current_step']->bundle,
'label',
), $vars['current_step']->label);
$vars['step_description'] = msnf_translate(array(
$vars['current_step']->step_name,
$vars['current_step']->bundle,
'description',
), $vars['current_step']->description);
$vars['step_classes'] = implode(' ', $vars['step_classes_array']);
// Which step is this (number).
$vars['current_position'] = array_search($vars['current_step']->step_name, array_keys($vars['steps'])) + 1;
$vars['step_count'] = count($vars['steps']);
}
/**
* Get all form steps.
*
* @param <string> $entity_type
* The name of the entity.
* @param <string> $bundle
* The name of the bundle.
* @param <boolean> $reset
* Whether to reset the cache or not.
* @param <array> $form
* The form the steps are (or will be) attached to.
* @param <array> $form
* The form the steps are (or will be) attached to.
*
* @return <array>
* List of all defined steps for the given entity type and bundle.
*/
function msnf_info_steps($entity_type, $bundle = NULL, $reset = FALSE, $form = array(), $form_state = array()) {
$steps_cached =& drupal_static(__FUNCTION__, FALSE);
if (!$steps_cached || $reset) {
if (!$reset && ($cached = cache_get('msnf_steps', 'cache_field'))) {
$steps_cached = $cached->data;
}
else {
$steps_cached = msnf_read_steps();
cache_set('msnf_steps', $steps_cached, 'cache_field');
}
}
// Allow other modules to alter step information.
$context = array(
'form' => $form,
'form_state' => $form_state,
'entity_type' => $entity_type,
'bundle' => $bundle,
);
drupal_alter('msnf_info_steps', $steps_cached, $context);
if (isset($steps_cached[$entity_type])) {
if (isset($steps_cached[$entity_type][$bundle])) {
return $steps_cached[$entity_type][$bundle];
}
elseif ($bundle === NULL) {
return $steps_cached[$entity_type];
}
}
return array();
}
/**
* Read all form steps.
*
* @param <array> $params
* Parameters for the query
* - $name The name of the entity.
* - $bundle The name of the bundle.
*
* @return <array>
* Array with all available steps keyed by entity type.
*/
function msnf_read_steps($params = array()) {
$steps = array();
ctools_include('export');
if (empty($params)) {
$records = ctools_export_load_object('msnf_step');
}
else {
$records = ctools_export_load_object('msnf_step', 'conditions', $params);
}
foreach ($records as $step) {
// Deleted form steps.
if (isset($step->disabled) && $step->disabled) {
continue;
}
$steps[$step->entity_type][$step->bundle][$step->step_name] = msnf_unpack($step);
}
return $steps;
}
/**
* Checks if a form step exists in required context.
*
* @param <string> $step_name
* The name of the form step.
* @param <string> $entity_type
* The name of the entity.
* @param <string> $bundle
* The bundle for the entity.
*/
function msnf_step_exists($step_name, $entity_type, $bundle) {
$steps = msnf_read_steps();
return !empty($steps[$entity_type][$bundle][$step_name]);
}
/**
* Unpacks a database row in a Step object.
* @param <object> $step
* Database result object with stored step data.
* @return <object> $step
* A form step object.
*/
function msnf_unpack($step) {
// Extract unserialized data.
if (isset($step->data)) {
$data = $step->data;
unset($step->data);
$step->label = $data['label'];
$step->weight = $data['weight'];
$step->children = $data['children'];
$step->format_type = !empty($data['format_type']) ? $data['format_type'] : 'default';
if (isset($data['format_settings'])) {
$step->format_settings = $data['format_settings'];
}
if (module_exists('i18n_string')) {
$identifier = sprintf('msnf_step:%s:%s:label', $step->step_name, $step->bundle);
$step->label = i18n_string_translate($identifier, $step->label);
}
}
return $step;
}
/**
* Packs a Step object into a database row.
* @param <object> $step
* The object to pack.
* @return <object> $record
* Database row object, ready to be inserted/updated.
*/
function msnf_pack($step) {
$record = clone $step;
$record->data = array(
'label' => $record->label,
'weight' => $record->weight,
'children' => $record->children,
'format_type' => !empty($record->format_type) ? $record->format_type : 'default',
);
if (isset($record->format_settings)) {
$record->data['format_settings'] = $record->format_settings;
}
return $record;
}
/**
* Delete a single step.
*
* @param <object> $step
* A step definition.
* @param <boolean> $ctools_crud
* Whether this function is called by the ctools crud delete.
*/
function msnf_step_export_delete($step, $ctools_crud = TRUE) {
$query = db_delete('msnf_step');
if (isset($step->identifier)) {
$query
->condition('identifier', $step->identifier);
if (!$ctools_crud) {
ctools_export_crud_disable('msnf_step', $step->identifier);
}
}
elseif (isset($step->id)) {
$query
->condition('id', $step->id);
}
$query
->execute();
cache_clear_all('msnf_steps', 'cache_field');
module_invoke_all('msnf_delete_step', $step);
}
/**
* Saves a single step definition.
*
* @param <object> $step
* The step definition to save.
*/
function msnf_step_save(&$step) {
// Prepare the record.
$object = msnf_pack($step);
if (isset($object->export_type) && $object->export_type & EXPORT_IN_DATABASE) {
// Existing record.
$update = array(
'id',
);
module_invoke_all('msnf_update_step', $object);
}
else {
// New record.
$update = array();
$object->export_type = EXPORT_IN_DATABASE;
module_invoke_all('msnf_create_step', $object);
}
// Write the record.
return drupal_write_record('msnf_step', $object, $update);
}
/**
* Function to retrieve all format possibilities for the form steps.
*/
function msnf_formatter_info() {
$cache =& drupal_static(__FUNCTION__, array());
if (empty($cache)) {
if ($cached = cache_get('msnf_formatter_info', 'cache_field')) {
$formatters = $cached->data;
}
else {
$formatters = array();
$formatters += module_invoke_all('msnf_formatter_info');
cache_set('msnf_formatter_info', $formatters, 'cache_field');
}
$cache = $formatters;
}
return $cache;
}
/**
* Attach steps to the (form) build.
*
* @param <array> $element
* The part of the form.
*/
function msnf_attach_steps(&$element, &$form_state) {
$entity_type = $element['#entity_type'];
$bundle = $element['#bundle'];
// Retrieve step information for this entity type and bundleand attach it to
// the element.
$steps = msnf_info_steps($entity_type, $bundle, $element, $form_state);
$element['#steps'] = _msnf_remove_empty_steps($steps, $element, $form_state);
// Sort steps by weight.
uasort($element['#steps'], '_msnf_step_sort');
// Create a lookup array.
$step_children = array();
foreach ($element['#steps'] as $step_name => $step) {
foreach ($step->children as $child) {
$step_children[$child] = $step_name;
}
}
$element['#step_children'] = $step_children;
// Add a pre render callback.
$element['#pre_render'][] = 'msnf_build_pre_render';
}
/**
* Process callback.
*/
function msnf_build_pre_render($element) {
// Skip the nesting and step functions if no steps are defined.
// This could be because you don't see them in the UI or programmatically.
if (empty($element['#steps'])) {
return $element;
}
// Nest the fields in the corresponding steps.
msnf_fields_nest($element);
// Allow others to alter the pre_rendered build.
drupal_alter('msnf_build_pre_render', $element);
return $element;
}
/**
* Recursive function to nest fields in the steps.
*
* This function will take out all the elements in the form and
* place them in the correct container element.
*
* @param <array> $element
* The current element to analyse.
*/
function msnf_fields_nest(&$element) {
// Create all steps and keep a flat list of references to these steps.
$step_references = array();
foreach ($element['#steps'] as $step_name => $step) {
$element[$step_name] = array();
$step_references[$step_name] =& $element[$step_name];
}
// Move all children to their parents. Use the flat list of references for
// direct access as we don't know where in the root_element hierarchy the
// parent currently is situated.
foreach ($element['#step_children'] as $child_name => $parent_name) {
// Block denied fields (#access) before they are put in groups.
// Fields (not groups) that don't have children (like field_permissions) are removed
// in field_group_field_group_build_pre_render_alter.
if (isset($element[$child_name]) && (!isset($element[$child_name]['#access']) || $element[$child_name]['#access'])) {
// If this is a group, we have to use a reference to keep the reference
// list intact (but if it is a field we don't mind).
$step_references[$parent_name][$child_name] =& $element[$child_name];
}
// The child has been copied to its parent: remove it from the root element.
unset($element[$child_name]);
}
// Bring extra element wrappers to achieve a grouping of fields.
// This will mainly be prefix and suffix altering.
foreach ($element['#steps'] as $step_name => $step) {
msnf_pre_render($step_references[$step_name], $step, $element);
}
}
/**
* Function to pre render the step element.
*
* @param <array> $element
* Element to render.
* @param <object> $step
* The current step object.
* @param <array> $form
* The form that contains the element.
*
* @see msnf_fields_nest()
*/
function msnf_pre_render(&$element, $step, &$form) {
// Only run the pre_render function if the step has elements.
if ($element == array()) {
return;
}
// Let modules define their wrapping element.
// Note that the step element has no properties, only elements.
foreach (module_implements('msnf_step_pre_render') as $module) {
$function = $module . '_msnf_step_pre_render';
if (function_exists($function)) {
// The intention here is to have the opportunity to alter the
// elements, as defined in hook_msnf_formatter_info.
// Note, implement $element by reference!
$function($element, $step, $form);
}
}
// Allow others to alter the pre_render.
drupal_alter('msnf_pre_render', $element, $step, $form);
}
/**
* Custom validation callback.
*
* @param <array> $form
* Form to validate.
* @param <array> $form_state
* Current form state.
*/
function msnf_entity_form_validate($form, &$form_state) {
if (!isset($form['#steps']) || count($form['#steps']) == 0) {
// Nothing to do here.
return;
}
if (!isset($form_state['triggering_element']['#name']) || !in_array($form_state['triggering_element']['#name'], array(
'previous',
'skip',
'next',
))) {
return;
}
$form_state['rebuild'] = TRUE;
$form_state['cache'] = TRUE;
if (isset($form_state['node'])) {
// Update node object based on submitted form values.
entity_form_submit_build_entity('node', $form_state['node'], $form, $form_state);
}
if (($current_step = _msnf_form_step_get_current($form, $form_state)) === FALSE) {
// Step not found. Do nothing.
return;
}
$step_names = array_keys($form['#steps']);
$step_index = array_search($current_step->step_name, $step_names);
if ($form_state['triggering_element']['#name'] == 'next') {
$step_index++;
}
elseif ($form_state['triggering_element']['#name'] == 'skip') {
$step_index += 2;
}
elseif ($form_state['triggering_element']['#name'] == 'previous') {
$step_index--;
}
if (isset($step_names[$step_index])) {
// Set name of next step to display.
$form_state['storage']['step'] = $step_names[$step_index];
}
else {
// Display first step.
$form_state['storage']['step'] = array_shift($step_names);
}
}
/**
* Helper function to get the current form step from $form_state.
*
* @param <array> $form
* The form where the step has been defined.
* @param <array> $form_state
* Current form state.
*
* @return <object>
* The current form step or <code>FALSE</code> if the step could not be found.
*/
function _msnf_form_step_get_current($form, $form_state) {
if (!isset($form['#steps']) || count($form['#steps']) == 0) {
// Nothing to do here.
return FALSE;
}
// Get all defined steps for this form.
$steps = $form['#steps'];
if (!isset($form_state['storage']['step'])) {
return array_shift($steps);
}
if (isset($steps[$form_state['storage']['step']])) {
return $steps[$form_state['storage']['step']];
}
// Fallback, step not found.
return FALSE;
}
/**
* Helper method to set the current form step.
*
* The step is set to active only, if all preceding steps are skippable. If one
* of the preceding steps is not skippable the first skippable step before the
* requested is activated.
*
* @param <string> $step_name
* Name of step to set active.
* @param <array> $form
* Reference to current entity form.
* @param <array> $form_state
* Reference to current form state.
* @param <int> $limit
* Maximum number of function calls to prevent recursion. Internally used
* only.
*/
function _msnf_form_step_set_current($step_name, &$form, &$form_state, $limit = 10) {
if (!empty($form_state['triggering_element']) || $limit <= 0) {
// Do not set the current step if the form has been submitted or the
// recursion limit has been reached.
return;
}
$step_names = isset($form['#steps']) ? array_keys($form['#steps']) : array();
if (!in_array($step_name, $step_names) || !isset($form_state['storage']['step'])) {
return;
}
// Check if there is any step before the requested one that has empty
// required fields.
$skippable_steps = _msnf_steps_get_skippable($form, $form_state);
$steps_required = array_values(array_diff($step_names, array_keys($skippable_steps)));
// Position of step in all steps.
$step_position = array_search($step_name, $step_names);
// Get position of first required step.
$first_required_step = reset($steps_required);
$step_position_required = array_search($first_required_step, $step_names);
if ($step_position > $step_position_required || count($steps_required) > 0 && $step_position === FALSE) {
// Try to activate the first required step.
_msnf_form_step_set_current($first_required_step, $form, $form_state, --$limit);
}
else {
// The requested step is not in or at first position of the list of
// required steps.
$form_state['storage']['step'] = $step_name;
}
}
/**
* Helper method to add the required buttons to a form.
*
* @param <array> $form
* The form where the buttons will be attached to.
* @param <array> $form_state
* Current form state.
*/
function _msnf_form_attach_buttons(&$form, &$form_state) {
$steps = $form['#steps'];
if (($current_step = _msnf_form_step_get_current($form, $form_state)) === FALSE) {
// Step not found. Do nothing.
return;
}
$step_settings = $current_step->format_settings['instance_settings'];
// Translate button labels.
foreach ($step_settings['buttons'] as $key => $label) {
if (empty($label)) {
continue;
}
$step_settings['buttons'][$key] = msnf_translate(array(
$current_step->step_name,
$current_step->bundle,
"button_{$key}",
), $label);
}
// Try to load previous and next step.
$previous_step = FALSE;
$next_step = FALSE;
$step_names = array_keys($steps);
$current_index = array_search($current_step->step_name, $step_names);
if ($current_index > 0 && isset($step_names[$current_index - 1])) {
// Seems we have a step before the current one.
$previous_step = $steps[$step_names[$current_index - 1]];
}
if ($current_index < count($steps) - 1 && isset($step_names[$current_index + 1])) {
// Seems we have a step after the current one.
$next_step = $steps[$step_names[$current_index + 1]];
}
// Update button labels if needed.
if ($next_step && $step_settings['buttons']['next'] !== FALSE && drupal_strlen($step_settings['buttons']['next']) == 0) {
// Set label of step as button label.
$step_settings['buttons']['next'] = msnf_translate(array(
$next_step->step_name,
$next_step->bundle,
'label',
), $next_step->label);
}
if ($previous_step && $step_settings['buttons']['previous'] !== FALSE && drupal_strlen($step_settings['buttons']['previous']) == 0) {
// Set label of step as button label.
$step_settings['buttons']['previous'] = msnf_translate(array(
$previous_step->step_name,
$previous_step->bundle,
'label',
), $previous_step->label);
}
// Todo: 'actions' is not defined in all entity forms (e.g. term).
if (!isset($form['actions'])) {
$form['actions'] = array(
'#type' => 'actions',
);
}
// Create a list of all buttons the form may have.
if ($current_index > 0 && $step_settings['buttons']['previous'] !== FALSE) {
// Add "previous" button.
$form['actions']['previous'] = array(
'#type' => 'submit',
'#access' => TRUE,
'#value' => $step_settings['buttons']['previous'],
'#weight' => 4,
'#name' => 'previous',
'#submit' => array(),
'#limit_validation_errors' => array(),
);
}
// Get the list of steps that may be skipped (no required fields and admin
// decided to make this step skippable.
$skippable_steps = _msnf_steps_get_skippable($form, $form_state);
// Get list of remaining steps.
$remaining_steps = array_slice($step_names, $current_index + 1);
if ($current_index < count($steps) - 1) {
// Add "next" button.
if ($step_settings['buttons']['next'] !== FALSE) {
$form['actions']['next'] = array(
'#type' => 'submit',
'#access' => TRUE,
'#value' => $step_settings['buttons']['next'],
'#weight' => 7,
'#name' => 'next',
);
}
// Is skipping the next step allowed?
$skip_next = isset($skippable_steps[$next_step->step_name]);
$skip_next &= $next_step->format_settings['instance_settings']['skip_non_required'];
// The last step may never be skipped.
$skip_next &= $current_index < count($steps) - 2;
if ($skip_next && $step_settings['buttons']['skip'] !== FALSE) {
// Add button to skip the next step.
$form['actions']['skip'] = array(
'#type' => 'submit',
'#access' => TRUE,
'#value' => $step_settings['buttons']['skip'],
'#weight' => 8,
'#name' => 'skip',
);
// Todo. If all further steps may be skipped ...
}
}
if (isset($form['actions']['submit']) && $current_index != count($steps) - 1) {
// Hide save button until we display the last step or all further steps may
// be skipped.
$form['actions']['submit']['#access'] = count(array_diff($remaining_steps, array_keys($skippable_steps))) == 0;
}
}
/**
* Hide all fields that are not associated to the current step.
*
* @param <array> $form
* Form to hide the fields from.
* @param <array> $form_state
* Current form state.
*/
function _msnf_hide_fields(&$form, &$form_state) {
if (($current_step = _msnf_form_step_get_current($form, $form_state)) === FALSE) {
// Step not found. Do nothing.
return;
}
// Get the names of all form children.
$form_elements = element_children($form);
// Hide all elements that do not belong to the current step.
foreach ($form_elements as $element_name) {
if ('title' !== $element_name && (empty($form[$element_name]['#type']) || in_array($form[$element_name]['#type'], array(
'value',
'actions',
'token',
'hidden',
)))) {
// Value elements and actions needs to be present on each step.
continue;
}
if (isset($form['#step_children'][$element_name]) && $form['#step_children'][$element_name] == $current_step->step_name) {
// Element is a child of the current step.
continue;
}
if (!empty($form[$element_name]['#group']) && in_array($form[$element_name]['#group'], $current_step->children)) {
// Element is part of a group within this step. Display the element.
continue;
}
// Maybe the element is in a field group?
if (module_exists('field_group') && isset($form['#fieldgroups'])) {
foreach ($form['#fieldgroups'] as $group_name => $group) {
if (isset($form['#step_children'][$group_name]) && $form['#step_children'][$group_name] == $current_step->step_name && _msnf_fieldgroup_has_element($form['#fieldgroups'], $group, $element_name)) {
// Element is a child of a fieldgroup which is a child of this step.
continue 2;
}
}
}
// Hide the element.
$form[$element_name]['#access'] = FALSE;
// Make sure the element is not required at this time!
_msnf_element_unset_required($form[$element_name]);
}
// Todo: hide preview on step change.
}
/**
* Helper function to check whether an element is nested within a fieldgroup or
* one of the fieldgroups child groups.
*
* @param <array> $groups
* Array with all available groups (needed for recursion).
* @param <object> $group
* Group to search for the element.
* @param <string> $element_name
* Name of form element to find in $group.
* @param <int> $depth
* Maximum number of levels to recurse. Internally used only.
*
* @return <boolean>
* TRUE if $element_name is in $group or one of its child groups, otherwise
* FALSE.
*/
function _msnf_fieldgroup_has_element($groups, $group, $element_name, $depth = 10) {
if ($depth <= 0) {
// Recursion limit reached.
return FALSE;
}
$in_group = FALSE;
// Element is direct child of group?
$in_group = $in_group || in_array($element_name, $group->children);
if (!$in_group) {
// Check child groups.
foreach ($group->children as $child_group_name) {
if (isset($groups[$child_group_name])) {
$in_group = $in_group || _msnf_fieldgroup_has_element($groups, $groups[$child_group_name], $element_name, --$depth);
}
}
}
return $in_group;
}
/**
* Helper method to make an element and all its children optional.
*
* @param <array> $element
* The element to make optional.
* @param <int> $depth
* Maximum depth of children to process. Internally used.
*/
function _msnf_element_unset_required(&$element, $depth = 10) {
if (isset($element['#required'])) {
$element['#required'] = FALSE;
}
if ($depth <= 0) {
// Make sure we do not recurse forever.
return;
}
foreach (element_children($element) as $key) {
_msnf_element_unset_required($element[$key], --$depth);
}
}
/**
* Restore field values.
*
* @param <array> $form
* The form to restore the values on.
* @param <array> $form_state
* Current form state.
*/
function _msnf_restore_values(&$form, &$form_state) {
if (isset($form_state['node'])) {
$form += _field_invoke_default('form', 'node', $form_state['node'], $form, $form_state);
}
}
/**
* Helper function to get all steps that may be skipped.
*
* @param array $form
* The form containing the steps to be checked.
*
* @return <array>
* Array with all skippable steps for the given form.
* @param <array> $form_state
* Current form state.
*/
function _msnf_steps_get_skippable($form, $form_state) {
if (!isset($form['#steps']) || count($form['#steps']) == 0) {
// Nothing to do here.
return array();
}
$skippable = array();
foreach ($form['#steps'] as $step_name => $step) {
if ($step->format_settings['instance_settings']['skip_non_required']) {
$has_required = FALSE;
// Check elements attached to this step.
foreach ($step->children as $element) {
if (!isset($form[$element]) && !isset($form['#fieldgroups'][$element])) {
continue;
}
if (isset($form['#fieldgroups'][$element])) {
// Element is a fieldgroup so process its children.
foreach ($form['#group_children'] as $element_name => $group) {
if ($group == $element && isset($form[$element_name])) {
$has_required = $has_required || _msnf_element_required($form, $element_name);
}
}
}
else {
$has_required = $has_required || _msnf_element_required($form, $element);
}
}
if ($has_required == FALSE) {
$skippable[$step_name] = $step;
}
}
}
// Allow other modules to alter the list of skippable steps.
$context = array(
'form' => $form,
'form_state' => $form_state,
);
drupal_alter('msnf_steps_skippable', $skippable, $context);
return $skippable;
}
/**
* Helper function to remove steps with no (accessible) fields.
*
* @param <array> $steps
* List of step objects.
* @param <array> $form
* The form where the step has been defined.
*
* @return <array>
* List of steps with at least one accessible field.
*/
function _msnf_remove_empty_steps($steps, $form, $form_state) {
global $user;
$steps_filtered = array();
foreach ($steps as $step_name => $step) {
if (empty($step->format_settings['instance_settings']['hide_if_empty'])) {
// Step should not be hidden if empty so skip processing.
$steps_filtered[$step_name] = $step;
continue;
}
if (count($step->children) == 0) {
// Step does not have any elements attached to it. Hide it!
continue;
}
$access = FALSE;
foreach ($step->children as $field_name) {
if (($field = field_info_field($field_name)) !== NULL) {
$access |= field_access('view', $field, 'node', $form_state['node']);
}
if (isset($form[$field_name]['#access'])) {
$access |= $form[$field_name]['#access'];
}
else {
// If an element does not have the #access-attribute we assume it is
// accessible.
$access = TRUE;
break;
// No further processing needed.
}
}
if ($access) {
// At least one field is accessible so add this step.
$steps_filtered[$step_name] = $step;
}
}
return $steps_filtered;
}
/**
* Helper function to test if an element is required for form submissions.
*
* If the field already has a value (e.g. while editing the node) it is not
* required anymore!
*
* @param <array> $form
* Form that contains $element_name.
* @param <string> $element_name
* Name of element to check.
*
* @return <boolean>
* TRUE if the element is required, otherwise FALSE.
*/
function _msnf_element_required($form, $element_name) {
$element = $form[$element_name];
$required = isset($element['#required']) ? $element['#required'] : FALSE;
// Check if the element has a value (equals to non-required).
$field_name = isset($element['#field_name']) ? $element['#field_name'] : $element_name;
$field_info = field_info_field($field_name);
if (is_array($field_info)) {
$empty_function = $field_info['module'] . '_field_is_empty';
if (function_exists($empty_function) && isset($form['#entity_type']) && isset($form['#entity'])) {
if (($items = field_get_items($form['#entity_type'], $form['#entity'], $element_name)) !== FALSE && count($items) > 0) {
foreach ($items as $value) {
if ($empty_function($value, $field_info) == FALSE) {
$required = FALSE;
}
}
}
else {
foreach (element_children($element) as $key) {
$required = $required || _msnf_element_required($element, $key);
}
}
}
}
else {
if ($element_name == 'title' && isset($element['#title'])) {
// If title is set, it is no longer required for us.
$required = isset($element['#default_value']) && is_scalar($element['#default_value']) ? drupal_strlen($element['#default_value']) == 0 : TRUE;
}
else {
$required = FALSE;
}
}
return $required;
}
/**
* Function used by uasort to sort objects (steps) by weight.
*
* @param <object> $a
* Base object to use for comparison.
* @param <object> $b
* Object to compare to.
*
* @return <int>
* Indicator whether the weight of $a is smaller, equal or greater than that
* of $b.
*/
function _msnf_step_sort($a, $b) {
$a_weight = is_object($a) && isset($a->weight) ? $a->weight : 0;
$b_weight = is_object($b) && isset($b->weight) ? $b->weight : 0;
if ($a_weight == $b_weight) {
return 0;
}
return $a_weight < $b_weight ? -1 : 1;
}
/**
* Wrapper function for i18n_string() if i18n_string enabled.
*
* @param <string> $name
* Name of the string, like "msnf_step:<step_name>:<content_type>"
* @param <string> $string
* String in default language
*
* @return <string>
* Translated string (or original text if no translation exists).
*/
function msnf_translate($name, $string) {
if (!module_exists('i18n_string')) {
return filter_xss($string);
}
if (is_array($name)) {
$name = implode(':', $name);
}
$translation = i18n_string('msnf_step:' . $name, filter_xss($string));
return html_entity_decode($translation, ENT_QUOTES);
}
Functions
Name![]() |
Description |
---|---|
msnf_attach_steps | Attach steps to the (form) build. |
msnf_block_info | Implements hook_block_info(). |
msnf_block_view | Implements hook_block_view(). |
msnf_build_pre_render | Process callback. |
msnf_ctools_plugin_api | Implements hook_ctools_plugin_api(). |
msnf_element_info | Implements hook_element_info(). |
msnf_entity_form_validate | Custom validation callback. |
msnf_fields_nest | Recursive function to nest fields in the steps. |
msnf_field_attach_delete_bundle | Implements hook_field_attach_delete_bundle(). |
msnf_field_attach_form | Implements hook_field_attach_form(). |
msnf_field_attach_rename_bundle | Implements hook_field_attach_rename_bundle(). |
msnf_field_extra_fields | Implements hook_field_extra_fields(). |
msnf_formatter_info | Function to retrieve all format possibilities for the form steps. |
msnf_form_field_ui_field_overview_form_alter | Implements hook_form_FORM_ID_alter(). |
msnf_form_node_form_alter | Implements hook_form_FORM_ID_alter(). |
msnf_form_process_msnf_form_step | Creates a single form step. |
msnf_info_steps | Get all form steps. |
msnf_load_step | Loads a step definition. |
msnf_menu | Implements hook_menu(). |
msnf_msnf_formatter_info | Implements hook_msnf_formatter_info(). |
msnf_msnf_format_settings | Implements hook_msnf_format_settings(). If the step has no format settings, default ones will be added. |
msnf_msnf_format_summary | Implements hook_msnf_format_summary(). |
msnf_msnf_step_pre_render | Implements hook_msnf_step_pre_render(). |
msnf_pack | Packs a Step object into a database row. |
msnf_permission | Implements hook_permission(). |
msnf_preprocess_msnf_block_step_info | Define variables used in msnf-block-step-info.tpl.php. |
msnf_preprocess_msnf_form_step_info | Define variables used in msnf-form-step-info.tpl.php. |
msnf_pre_render | Function to pre render the step element. |
msnf_pre_render_prepare | Helper function to prepare basic variables needed for most formatters. |
msnf_read_steps | Read all form steps. |
msnf_step_exists | Checks if a form step exists in required context. |
msnf_step_export_delete | Delete a single step. |
msnf_step_menu_load | Menu Wildcard loader function to load step definitions. |
msnf_step_pre_render_default | Implements msnf_step_pre_render_<format-type>. Format type: 'default'. |
msnf_step_save | Saves a single step definition. |
msnf_theme | Implements hook_theme(). |
msnf_translate | Wrapper function for i18n_string() if i18n_string enabled. |
msnf_unpack | Unpacks a database row in a Step object. |
theme_msnf_form_step | Returns HTML for a single form step. |
_msnf_element_required | Helper function to test if an element is required for form submissions. |
_msnf_element_unset_required | Helper method to make an element and all its children optional. |
_msnf_fieldgroup_has_element | Helper function to check whether an element is nested within a fieldgroup or one of the fieldgroups child groups. |
_msnf_form_attach_buttons | Helper method to add the required buttons to a form. |
_msnf_form_step_get_current | Helper function to get the current form step from $form_state. |
_msnf_form_step_set_current | Helper method to set the current form step. |
_msnf_hide_fields | Hide all fields that are not associated to the current step. |
_msnf_remove_empty_steps | Helper function to remove steps with no (accessible) fields. |
_msnf_restore_values | Restore field values. |
_msnf_steps_get_skippable | Helper function to get all steps that may be skipped. |
_msnf_step_sort | Function used by uasort to sort objects (steps) by weight. |