'submit', '#value' => t('Save'), ); $form['#submit'][] = 'draggableviews_view_draggabletable_form_submit'; return $form; } /** * Implementing hook_submit */ function draggableviews_view_draggabletable_form_submit($vars) { // check permissions if (!user_access('Allow Reordering')) { drupal_set_message(t('You are not allowed to reorder nodes.'), 'error'); return; } // gather all needed information $view = $vars['#parameters'][2]->view; $results = $view->result; // get input $input = $vars['submit']['#post']; $info = _draggableviews_info($view); if (!isset($info['order'])) return; // loop through all resulting nodes foreach ($results AS $row) { // set order values if (isset($info['order']['fields'])) { // @todo: Why using [0] for input values. Antiquated? $info['input'][$row->nid]['order'][0] = $input[$info['order']['fields'][0]['field_name'] .'_'. $row->nid]; } // set parent values if (isset($info['hierarchy'])) { $info['input'][$row->nid]['parent'] = $input[$info['hierarchy']['field']['field_name'] .'_'. $row->nid]; } } // build hierarchy _draggableviews_build_hierarchy($info); // save structure _draggableviews_save_hierarchy($info); if (isset($info['hierarchy'])) { // save expanded/collapsed state global $user; foreach ($vars['submit']['#post'] AS $key => $val) { if (ereg('draggableviews_collapsed_', $key)) { $parent_nid = drupal_substr($key, 25); db_query("DELETE FROM {draggableviews_collapsed} WHERE uid=%d AND parent_nid=%d", $user->uid, $parent_nid); db_query("INSERT INTO {draggableviews_collapsed} (uid, parent_nid, collapsed) VALUES (%d, %d, %d)", $user->uid, $parent_nid, $val); } } } } /** * Collect all known information in a handy array * * @param $view * The views object * * @return * An structured array containt the extracted draggableviews settings * and additional field information. * array( * 'order' => array( * 'fields' => array( * 0 => array( * 'field_name' => field_name * 'field_alias' => field_alias * 'handler' => handler, * ), * .. * ), * 'visible' => True/False * 'minimum_value' => value * ), * 'hierarchy' => array( * 'field' => array( * 'field_name' => field_name, * 'field_alias' => field_alias, * 'handler' => handler, * ), * 'visible' => TRUE/FALSE, * ), * * 'types' = array( * node_type => "root"/"leaf", * ), * * 'expand_links' = array( * 'show' => TRUE/FALSE, * 'default_collapsed' => TRUE/FALSE, * ), * * 'nodes' => array( * nid1 => array( * field_name1 => value * field_name2 => value * .. * ), * .. * ), * ); */ function _draggableviews_info(&$view, $prepare_fields = TRUE, $prepare_options = FALSE, $prepare_results = TRUE) { $options = &$view->style_plugin->options; $fields = &$view->field; $results = &$view->result; // build information array $info = array(); $info['view'] = $view; if ($prepare_fields && isset($options['tabledrag_order'])) { foreach ($options['tabledrag_order'] as $field) { if ($handler = _draggableviews_init_handler($field, $view)) { $info['order']['fields'][] = array( 'handler' => $handler, 'field_name' => $field['field'], 'field_alias' => $fields[$field['field']]->field_alias, ); } else { drupal_set_message(t('Draggableviews: Handler ') . $field['handler'] . t(' could not be found.'), 'error'); unset($info['order']); return $info; } } $info['order']['visible'] = strcmp($options['tabledrag_order_visible']['visible'], 'visible') == 0 ? TRUE : FALSE; $info['order']['minimum_value'] = $info['order']['fields'][0]['handler']->get_minimum_value(); if (count($info['order']['fields']) >= 2 && $options['tabledrag_hierarchy']['field'] != 'none') { if ($handler = _draggableviews_init_handler($options['tabledrag_hierarchy'], $view)) { $info['hierarchy'] = array( 'field' => array( 'handler' => $handler, 'field_name' => $options['tabledrag_hierarchy']['field'], 'field_alias' => $fields[$options['tabledrag_hierarchy']['field']]->field_alias, ), 'visible' => strcmp($options['tabledrag_hierarchy_visible']['visible'], 'visible') == 0 ? TRUE : FALSE, ); if ($prepare_fields && isset($options['tabledrag_types'])) { foreach ($options['tabledrag_types'] as $type) { $info['types'][$type['node_type']] = $type['type']; } } if ($prepare_options) { $info['expand_links'] = array( 'show' => strcmp($options['tabledrag_expand']['expand_links'], 'expand_links') == 0 ? TRUE : FALSE, 'default_collapsed' => strcmp($options['tabledrag_expand']['collapsed'], 'collapsed') == 0 ? TRUE : FALSE, ); } } else { drupal_set_message(t('Draggableviews: Handler ') . $field['handler'] . t(' could not be found.'), 'error'); unset($info['hierarchy']); } } } $info['nodes'] = array(); if ($prepare_results && $prepare_fields && $options['tabledrag_order']) { $new_nodes = array(); // loop through all resulting nodes foreach ($results as $row) { $node = array(); foreach ($info['order']['fields'] as $field) { $node[$row->nid]['order'][] = $row->{$field['field_alias']}; } if (isset($info['hierarchy'])) { $node[$row->nid]['parent'] = $row->{$info['hierarchy']['field']['field_alias']}; } if ($node[$row->nid]['order'][0] == NULL || $node[$row->nid]['order'][0] <= $info['order']['minimum_value']) { $new_nodes += $node; } else { $info['nodes'] += $node; } } $info['nodes'] += $new_nodes; } return $info; } /** * Get instance from handler * * @param field * The field options specified in the style plugin * @param view * The view object * * @return * An instance of the handler */ function _draggableviews_init_handler($field, &$view) { if (isset($field['handler']) && ($handler_info = draggableviews_discover_handlers($field['handler'])) && file_exists($handler_info['path'] .'/'. $handler_info['file'])) { require_once($handler_info['path'] .'/'. $handler_info['file']); $handler = new $handler_info['handler']; $handler->init($field['field'], $view); return $handler; } else { return FALSE; } } /** * Analyze structure * * @param info * The structured information array * * @return * FALSE if structure is broken */ function _draggableviews_analyze_structure(&$info) { // calculate depth of all nodes. // If one of the parents of a node is broken, // the return will be FALSE if (!_draggableviews_calculate_depths($info)) { return FALSE; } // Detect order collisions and solve them // If one order of the same depth is found // twice the return will be FALSE. if (!_draggableviews_detect_order_collisions($info)) { return FALSE; } foreach ($info['nodes'] as $nid => $prop) { // Detect ordering errors // The nodes order value has to equal with the // parents order value. Otherwise the return will be FALSE. if (_draggableviews_check_order($nid, $info) == FALSE) { return FALSE; } } return TRUE; } /** * Build hierarchy * * This function also handles broken structures * * @param info * The structured information array */ function _draggableviews_build_hierarchy(&$info) { $nodes = &$info['nodes']; $input = &$info['input']; foreach ($nodes as $nid => $prop) { // get depth if (($depth = _draggableviews_get_hierarchy_depth($nid, $input, $info)) === FALSE) { // Error! The hierarchy structure is broken and could // look like the following: (we're currently processing X) // A // --X // --D // // The next steps: // 1) bring it down to the root level // 2) Set order fields to the minimum $nodes[$nid]['parent'] = 0; // We gracefully sidestep the order-loop $depth = -1; } // Let's take a look at the following expample, to understand // what is beeing done. // // A // --B // --C // --X // --D // E // Imagine we're currently processing X: // // We know that X is in depth=3, so we save the received // weight value in the 3rd order field of node X. // // The 2nd order field must inherit the received weight of // node C (the next parent). And the 1st order field must // inherit the received weight of node A (the parent of C). // // When we finally order the view by weight1, weight2, weight3 then // weight1 and weight2 from node X will always equal with // those from node A and B, and weight3 defines the order of the 3rd level. $temp_nid = $nid; for ($i = $depth; $i >= 0; $i--) { // we're operating top-down, so we determine the parents nid by the way $nodes[$nid]['order'][$i] = $input[$temp_nid]['order'][0]; if (isset($info['hierarchy']) && $i > 0) { if (!($temp_nid = $input[$temp_nid]['parent'])) { // this line should never be reached assumed the depth // was calculated correctly. drupal_set_message(t('Undefined State called in draggableviews_build_hierarchy(..)'), 'error'); break; } } } if (isset($info['hierarchy']) && $depth > -1) { // Simply set the parent value $nodes[$nid]['parent'] = $input[$nid]['parent']; } // Now set all unused weights to a minimum value. Otherwise // it could happen that a child appears above its parent. // The ? can be anything, unfortunately also > 5 // // --A (3,5) // B (3,?) // // To guaranteer that the ? is always the lowest, we choose // the minimum. // Due to this it's recommended that all order fields have // the same minimum value! @todo: WHY? DOESN'T MAKE SENSE ANY MORE $depth = ($depth == -1) ? 0 : $depth; for ($i = $depth + 1; $i < count($info['order']['fields']); $i++) { $info['nodes'][$nid]['order'][$i] = $info['order']['fields'][$i]['handler']->get_minimum_value(); } } } /** * Rebuild hierarchy * * This function is called when the structure is broken * * @param info * The structures information array */ function _draggableviews_rebuild_hierarchy(&$info) { // count recursions static $recursion_counter; if (!isset($recursion_counter)) $recursion_counter = 0; $recursion_counter++; //drupal_set_message("Draggableviews: Rebuilding structure.."); $nodes = &$info['nodes']; $info['input'] = array(); $input = &$info['input']; // If the items to display per page are limitated, we have to make sure that // there's no hidden node with an order value that refers to the current page. // This can be achieved by simply assigning ascending order values. if ($info['view']->pager['items_per_page'] > 0) { // load full view and assign ascending numbers $view = $info['view']->clone_view(); $view->pager['items_per_page'] = 0; $view->executed = FALSE; $view->execute(); // We preserve the old info array and just change the view object which contains // the results. We're not allowed to build an info array from scratch because there // could be a difference between current settings and saved settings view (preview mode). $temp_info = $info; $temp_info['view'] = $view; // assign ascending numbers and save hierarchy _draggableviews_number_serially($temp_info); _draggableviews_save_hierarchy($temp_info); return true; } // Build an input-array to simulate the form data we would receive on submit // loop through all nodes foreach ($nodes as $nid => $prop) { $depth = $prop['depth'] ? $prop['depth'] : 0; // use order weight of the hierarchy level the field is situated in $input[$nid][0] = $prop['order'][$depth]; $input[$nid]['order'][0] = $prop['order'][$depth]; if (isset($info['hierarchy'])) { // set parent $input[$nid]['parent'] = $prop['parent']; } } // build hierarchy _draggableviews_build_hierarchy($info); // save hierarchy _draggableviews_save_hierarchy($info); // analyze structure (also calculates depths) if (!_draggableviews_analyze_structure($info)) { // the structure is broken if ($recursion_counter <= 10) { // start recursion return _draggableviews_rebuild_hierarchy($info); } else { //drupal_set_message(t('Reached limit of '. $recursion_counter .' recursions')); return FALSE; } } else { //drupal_set_message(t('Solved all problems after '. $recursion_counter .' recursions')); // redirect here return TRUE; } } /** * Detect and repair order collisions * * @return * TRUE if no collision detected */ function _draggableviews_detect_order_collisions(&$info) { $nodes = &$info['nodes']; $collision = FALSE; // Detect order collisions // Check for the following: // 1) The minimum value should not be used as it // should be possible for new nodes to default on top // without order collisions // 2) @todo The last value should not be used... // 3) An order value should only be used once per depth/parent // array( // parent => array(order1, ..), // .. // ) $order = array(); $min_value = $info['order']['minimum_value']; // loop through all nodes foreach ($nodes as $nid => $prop) { if (isset($info['hierarchy'])) { $parent = $prop['parent']; // if no parent set use 0 if (!isset($parent)) $parent = 0; } else { $parent = 0; } $order_field_value = &$nodes[$nid]['order'][$prop['depth']]; $begin_search = TRUE; // make sure that the minimum value cannot be used if (!is_array($order[$parent])) $order[$parent] = array($min_value); if (isset($order_field_value)) { // first try to keep current value $tmp_order = $order_field_value; } else { // if no value set, default to the minimum value. // So the next free value will be assigned automatically $tmp_order = $min_value; } while (is_numeric(array_search($tmp_order, $order[$parent]))) { // if there already exists an order value for this depth/parent // we have to find another one. // Try to find a free order: if ($begin_search == TRUE) { $tmp_order = $min_value + 1; $begin_search = FALSE; } else { $tmp_order++; } $collision = TRUE; } $order[$parent] = array_merge(array($tmp_order), $order[$parent]); $order_field_value = $tmp_order; } return !$collision; } /** * Set values and save nodes * * @param $info * The structured information array */ function _draggableviews_save_hierarchy($info) { // loop through all nodes foreach ($info['nodes'] as $nid => $prop) { if (isset($info['hierarchy'])) { $value = $prop['parent']; $handler = $info['hierarchy']['field']['handler']; $handler->save($nid, $value); } for ($i = 0; $i < count($info['order']['fields']); $i++) { // loop through all order fields $value = $prop['order'][$i]; $handler = $info['order']['fields'][$i]['handler']; $handler->save($nid, $value); } } } /** * Check order settings * * @param nid * The node id to check * @param info * The structured information array * * @return * TRUE if no errors were found */ function _draggableviews_check_order($nid, &$info) { $nodes = &$info['nodes']; $temp_nid = $nid; for ($i = $nodes[$nid]['depth']; $i >= 0; $i--) { // we're operating top-down, so we determine the parents nid by the way if ($nodes[$nid]['order'][$i] != $nodes[$temp_nid]['order'][$i]) { return FALSE; } if (isset($info['hierarchy']) && $i > 0) { if (!(isset($nodes[$temp_nid]) && ($temp_nid = $nodes[$temp_nid]['parent']))) { // this line should never be reached assumed the depth // was calculated correctly. drupal_set_message(t('Undefined State called in draggableviews_build_hierarchy(..)'), 'error'); return FALSE; } } } return TRUE; } /** * Calculate depth of all nodes * * @param info * The structured information array * * @return * TRUE if no errors were found */ function _draggableviews_calculate_depths(&$info) { $error = FALSE; // loop through all rows the view returns foreach ($info['nodes'] as $nid => $prop) { // determine hierarchy depth of current row $info['nodes'][$nid]['depth'] = _draggableviews_get_hierarchy_depth($nid, $info['nodes'], $info); if ($info['nodes'][$nid]['depth'] === FALSE) $error = TRUE; } return !$error; } /** * Get Hierarchy depth * * This function detects cycles, * broken hierarchy structures * and wrong weight settings * * @param $node * The node from wich we want to know the depth * @param $info * The structured information array * return * The hierarchy depth, * on error FALSE. */ function _draggableviews_get_hierarchy_depth($nid, &$nodes, &$info) { if (!isset($info['hierarchy'])) return 0; $depth = 0; $error = FALSE; $temp_nid = $nid; $used_nids = array(); $used_nids[] = $temp_nid; while (!($error = !isset($nodes[$temp_nid])) && ($temp_nid = $nodes[$temp_nid]['parent']) > 0) { // In order to detect cycles we use an array, // where all used node ids are saved in. If any node id // occurs twice -> return FALSE $cycle_found = array_search($temp_nid, $used_nids); if (isset($cycle_found) && $cycle_found !== FALSE) { drupal_set_message(t("Draggableviews: A cycle was found.")); return FALSE; } $used_nids[] = $temp_nid; $depth++; } if ($error) { // If loop breaked caused by an error return FALSE; } return $depth; } /* * filter handlers by type * * @param $type * the field type * @param $fields * the fields array * return * the available fields */ function _draggableviews_filter_fields($types = array(''), $handlers) { $available_fields = array(); foreach ($handlers as $field => $handler) { if ($label = $handler->label()) { $available_fields[$field] = $label; } else { $available_fields[$field] = $handler->ui_name(); } } return $available_fields; } /** * Assign ascending numbers * * @param $nodes * the nodes array to number */ function _draggableviews_number_serially(&$info) { // loop through all resulting nodes $minimum_value = $info['order']['fields'][0]['handler']->get_minimum_value(); for ($i = 0; $i < count($info['view']->result); $i++) { if (isset($info['order']['fields'])) { // Assign ascending order values, skipping the minimum value. $info['nodes'][$info['view']->result[$i]->nid]['order'][0] = $minimum_value + 1 + $i; } // set parent values if (isset($info['hierarchy'])) { // Bring all children down to the root level. $info['nodes'][$info['view']->result[$i]->nid]['parent'] = 0; } } } /** * Imlpementing hook_views_pre_render */ function draggableviews_views_pre_render(&$view) { $info = _draggableviews_info($view); if (!isset($info['order'])) return; // If click sort was used build order values manually if ($_GET['order']) { // assign ascending numbers _draggableviews_number_serially($info); _draggableviews_save_hierarchy($info); $view->executed = FALSE; $view->execute(); return; } // analyze structure (also calculates depths) // don't analyze if view hasn't been saved yet if (is_numeric($view->vid) && !_draggableviews_analyze_structure($info)) { // the structure is broken if (_draggableviews_rebuild_hierarchy($info)) { drupal_set_message('The structure was broken. It has been repaired successfully.'); // the structure has been repaired $view->executed = FALSE; $view->execute(); } else { drupal_set_message('The structure is broken. It could not be repaired.', 'error'); // the structure could not be repaired return; } } }