From 502b80f2dc45aad98d21b987474d42c55ddd722c Mon Sep 17 00:00:00 2001
From: Ron Shimshock <ron@shimshockgroup.com>
Date: Sat, 25 Nov 2017 16:01:26 -0600
Subject: [PATCH] Native handler sorting

---
 draggableviews.module                      | 130 +++++++++++++++++++++++++++++
 handlers/draggableviews_handler_native.inc |  22 ++++-
 2 files changed, 148 insertions(+), 4 deletions(-)

diff --git a/draggableviews.module b/draggableviews.module
index bd8d1e6..3f66166 100644
--- a/draggableviews.module
+++ b/draggableviews.module
@@ -372,6 +372,136 @@ function draggableviews_get_hierarchy_handlers() {
   return $return;
 }
 
+/**
+ * Sorts an array of items by their parents, then by their weights.
+ *
+ * This can be used by hierarchy handlers that store weights in the database
+ * differently from how Drupal's drag-and-drop sets them. For example, Drupal's
+ * drag-and-drop will set weights relative to each item's parent, so that a
+ * parent/child sorting may have weights that look like the following:
+ * - Item 1    (weight 0)
+ * - Item 2    (weight 1)
+ *   - Child 1 (weight 0)
+ *   - Child 2 (weight 1)
+ *   - Child 3 (weight 2)
+ *   - Child 4 (weight 3)
+ * - Item 3    (weight 2)
+ * But this module's native hierarchy handler uses absolute weights in its
+ * database storage (not relative ones), so the above weights cannot be saved
+ * since they would result in the view being out of order.
+ *
+ * This function sorts the provided array by parents (including by grandparents
+ * or beyond, if necessary), then by weight, so that the final array will be in
+ * the order shown above, and the hierarchy handler can then save the items to
+ * the database with weights reflective of that order.
+ *
+ * @param array $items
+ *   An array of items containing the 'weight' key, and optionally the 'id' and
+ *   'parent' keys (if a hierarchy exists). This function will sort the
+ *   provided array by reference.
+ *
+ * @see draggableviews_handler_native::set()
+ */
+function draggableviews_sort_by_parents_and_weights(&$items) {
+  // Add a 'parent_weights' element to each item which contains the weights of
+  // the item and all its parents, sorted by hierarchy. For example, if an item
+  // has weight 5, its parent has weight 2, and its grandparent has weight 3,
+  // this element will be set to array(3, 2, 5).
+  foreach ($items as &$item) {
+    // First add the item's weight itself.
+    $item['parent_weights'] = array($item['weight']);
+    $current_item = $item;
+    $finished = FALSE;
+    do {
+      // If the item has a parent, prepend the parent's weight to the array,
+      // then continue through the loop by checking its parent.
+      $parent_item = !empty($current_item['parent']) ? _draggableviews_get_parent_item($current_item, $items) : array();
+      if (isset($parent_item['weight'])) {
+        array_unshift($item['parent_weights'], $parent_item['weight']);
+        $current_item = $parent_item;
+      }
+      // Stop the loop when there are no more parents.
+      else {
+        $finished = TRUE;
+      }
+    } while (!$finished);
+  }
+
+  // Sort by the 'parent_weights' key, using a comparison function that will
+  // prioritize the entries in order of hierarchy.
+  uasort($items, '_draggableviews_sort_by_parents_and_weight');
+
+  // Remove the 'parent_weights' element so as not to pollute the calling code
+  // with it.
+  foreach ($items as &$item) {
+    unset($item['parent_weights']);
+  }
+}
+
+/**
+ * Returns the provided item's parent item, or an empty array if there is none.
+ *
+ * @param array $item
+ *   An array representing the item whose parent should be returned. It should
+ *   have a 'parent' key representing the ID of the parent item.
+ * @param array $items_to_search
+ *   An array representing the list of items within which the parent item will
+ *   be searched for. Each element should have an 'id' key identifying its ID.
+ *
+ * @return array
+ *   An array representing the parent item, or an empty array if the parent
+ *   cannot be found.
+ */
+function _draggableviews_get_parent_item($item, $items_to_search) {
+  foreach ($items_to_search as $item_to_search) {
+    if ($item_to_search['id'] == $item['parent']) {
+      return $item_to_search;
+    }
+  }
+  return array();
+}
+
+/**
+ * Callback for uasort() which sorts by the 'parent_weights' element.
+ *
+ * @param array $a
+ *   The first item for comparison. It should be an array with a
+ *   'parent_weights' element that contains an array of weights for this item's
+ *   parents and itself, in hierarchical order.
+ * @param array $b
+ *   The second item for comparison, similar to the above.
+ *
+ * @return int
+ *   If $a should appear before $b, -1 is returned. If $b should appear before
+ *   $a, 1 is returned. Otherwise, 0 is returned.
+ *
+ * @see draggableviews_sort_by_parents_and_weights()
+ */
+function _draggableviews_sort_by_parents_and_weight($a, $b) {
+  $a_weights = $a['parent_weights'];
+  $b_weights = $b['parent_weights'];
+  do {
+    if (!$a_weights && !$b_weights) {
+      return 0;
+    }
+    // If one of the arrays has run out of weights, put that item first. This
+    // ensures that parents will always be sorted before their children.
+    if ($a_weights && !$b_weights) {
+      return 1;
+    }
+    if ($b_weights && !$a_weights) {
+      return -1;
+    }
+    // Otherwise, compare the remaining weights in order of hierarchy (so that
+    // the higher-level parents are always given priority).
+    $a_weight = array_shift($a_weights);
+    $b_weight = array_shift($b_weights);
+    if ($a_weight != $b_weight) {
+      return ($a_weight < $b_weight) ? -1 : 1;
+    }
+  } while (TRUE);
+}
+
 /**
  * Ajax draggabletable submit handler.
  */
diff --git a/handlers/draggableviews_handler_native.inc b/handlers/draggableviews_handler_native.inc
index c34cbc8..62e3b52 100644
--- a/handlers/draggableviews_handler_native.inc
+++ b/handlers/draggableviews_handler_native.inc
@@ -14,8 +14,22 @@ $plugin = array(
 
 class draggableviews_handler_native extends draggableviews_handler {
   public function get($field, $index) {
-    $row = $field->view->result[$index];
-    return (isset($row->draggableviews_structure_weight_coalesce)) ? $row->draggableviews_structure_weight_coalesce : 0;
+    // New items that haven't been saved to the {draggableviews_structure}
+    // table yet won't have sensible weight values in the view result. For
+    // example, multiple items may have the same weight even though they are
+    // intended to appear in a specific order due to other sort criteria in the
+    // view, and that same weight can also be something nonsensical such as
+    // "2147483647" (see draggableviews_handler_sort::query()) that will
+    // obviously not match one of the available weights in the dropdown. Since
+    // there is no simple way to tell which weights in the view result are
+    // "real" and which aren't, and since the most important thing for this
+    // function to do is return weights that reflect the correct ordering,
+    // rather than weights that accurately match the database (especially since
+    // Drupal's drag-and-drop JavaScript will tend to reset all the weights on
+    // the form anyway), the best thing for this function to do is simply
+    // return the row index as the weight, which exactly matches what the
+    // drag-and-drop JavaScript will usually set it to.
+    return $index;
   }
 
   function set($form_state) {
@@ -43,8 +57,8 @@ class draggableviews_handler_native extends draggableviews_handler {
     // Save records to our custom table.
     $weight = 0;
 
-    // Reorder the items by weight.
-    uasort($fv['draggableviews'], 'drupal_sort_weight');
+    // Put the items in the correct order.
+    draggableviews_sort_by_parents_and_weights($fv['draggableviews']);
 
     foreach ($fv['draggableviews'] as $item) {
       // Make sure id is available.
-- 
2.15.0

