commit 3eeb1ed1da51f4dd1bda72afee2da018b76c68d6
Author: Jun Omae <jun66j5@gmail.com>
Date:   Thu Oct 4 18:10:41 2018 +0900

    Implement `order_fields` of `inputs_layout` and `header_layout` to fix layout bug with textarea and improve performance of chaning layout

diff --git a/dynfields/htdocs/layout.js b/dynfields/htdocs/layout.js
index 0d7c8269c..aeca1a40b 100755
--- a/dynfields/htdocs/layout.js
+++ b/dynfields/htdocs/layout.js
@@ -18,33 +18,30 @@ var Layout = function (name) {
 
   // Returns true of the field needs its own row
   this.needs_own_row = function (field) {
-    var $field = jQuery('#field-' + field);
-    if ($field.length)
-      return $field.is('TEXTAREA');
-    return false;
+    return jQuery('textarea#field-' + field).length !== 0;
   };
 
+  var saved_field_order = {};
+
   // Update the field layout given a spec
   this.update = function (spec) {
     var this_ = this;
+    var name = this.name;
 
     // save original field order
-    if (window.dynfields_orig_field_order == undefined)
-      window.dynfields_orig_field_order = Object();
-
-    if (window.dynfields_orig_field_order[this.name] == undefined) {
-      window.dynfields_orig_field_order[this.name] = [];
+    if (!(name in saved_field_order)) {
+      saved_field_order[name] = [];
       jQuery(this.selector).each(function (i, e) {
         var field = this_.get_field($(this));
         if (field)
-          window.dynfields_orig_field_order[this_.name].push(field);
+          saved_field_order[name].push(field);
       });
     }
 
     // get visible and hidden fields
     var visible = [];
     var hidden = [];
-    jQuery.each(window.dynfields_orig_field_order[this.name], function (i, field) {
+    jQuery.each(saved_field_order[name], function (i, field) {
       var tx = this_.get_tx(field);
       if (tx.hasClass('dynfields-hide')) {
         hidden.push(field);
@@ -91,6 +88,29 @@ var Layout = function (name) {
   }
 };
 
+var dynfields_group = function(values, n, callback) {
+  var groups = [];
+  var buf = [];
+  jQuery.each(values, function(index, name) {
+    if (callback(name)) {
+      if (buf.length !== 0) {
+        groups.push(buf.slice(0, n));
+        buf = [];
+      }
+      groups.push([name, true]);
+    } else {
+      buf.push(name);
+      if (buf.length === n) {
+        groups.push(buf.slice(0, n));
+        buf = [];
+      }
+    }
+  });
+  if (buf.length !== 0) {
+    groups.push(buf.slice(0, n));
+  }
+  return groups;
+};
 
 /*
  * Inputs Layout implementation
@@ -112,44 +132,56 @@ inputs_layout.get_field = function (td) {
   return input.attr('id').slice(6);
 };
 
-// move_field
-inputs_layout.move_field = function (field, i) {
-  var td = this.get_tx(field);
-  var th = td.prev('th');
-
-  // find correct row (tr) to insert field
-  var row = Math.round(i / 2 - 0.5); // round down
-  var $properties = jQuery('#properties');
-  row += $properties.find('td.fullrow').length; // skip fullrows
-  var tr = $properties.find('tr:eq(' + row + ')');
-
-  // find correct column (tx) to insert field
-  var col = 'col' + ((i % 2) + 1);
-  if (tr.find('th').length) {
-    if (col == 'col1') {
-      var old_th = tr.find('th:first');
-      if (old_th.get(0) != th.get(0)) { // don't move self to self
-        old_th.before(th);
-        old_th.before(td);
-      }
-    } else {
-      var old_td = tr.find('td:has(:input):last');
-      if (old_td.get(0) != td.get(0)) { // don't move self to self
-        old_td.after(td);
-        old_td.after(th);
+inputs_layout.order_fields = function (new_fields) {
+  var this_ = this;
+  var properties = jQuery('#properties');
+  var target_row = properties.find('textarea[name=field_description]')
+                             .closest('tr');
+  var cells = {};
+  var headers = {};
+  var fullrows = {};
+  jQuery.each(new_fields, function(idx, name) {
+    var cell = this_.get_tx(name);
+    cells[name] = cell;
+    headers[name] = cell.prev('th');
+    fullrows[name] = cell.hasClass('fullrow');
+  });
+  var groups = dynfields_group(new_fields, 2,
+                               function(name) { return fullrows[name] });
+  jQuery.each(groups, function(idx, group) {
+    var col1 = group[0];
+    var col2 = group[1];
+    var cell1 = cells[col1];
+    var header1 = headers[col1];
+    cell1.removeClass('col2');
+    header1.removeClass('col2');
+    cell1.addClass('col1');
+    header1.addClass('col1');
+    var row = jQuery('<tr>').append(headers[col1], cell1);
+    if (col2 !== true) {
+      var cell2, header2;
+      if (col2) {
+        cell2 = cells[col2];
+        header2 = headers[col2];
+        cell2.removeClass('col1');
+        header2.removeClass('col1');
+      } else {
+        cell2 = jQuery('<td>');
+        header2 = jQuery('<th>');
       }
+      header2.addClass('col2');
+      cell2.addClass('col2');
+      row.append(header2, cell2);
     }
-  } else {
-    // no columns so just insert
-    tr.append(th);
-    tr.append(td);
-  }
-
-  // let's set col
-  td.removeClass('col1 col2');
-  th.removeClass('col1 col2');
-  td.addClass(col);
-  th.addClass(col);
+    target_row.after(row);
+    target_row = row;
+  });
+  properties.find('> table > tbody > tr').each(function() {
+    var row = $(this);
+    var headers = row.children('th');
+    if (headers.length === 0 || !jQuery.trim(headers.text()))
+      row.remove();
+  });
 };
 
 
@@ -171,33 +203,48 @@ header_layout.get_field = function (th) {
   return (th.attr('id') ? th.attr('id').slice(2) : '');
 };
 
-// move_field
-header_layout.move_field = function (field, i) {
-  var th = this.get_tx(field);
-  var td = th.next('td');
-
-  // find correct row (tr) to insert field
-  var row = Math.round(i / 2 - 0.5); // round down
-  var tr = jQuery('#ticket').find('.properties tr:eq(' + row + ')');
-
-  // find correct column (tx) to insert field
-  if (tr.find('th').length) {
-    if (i % 2 == 0) {
-      var old_th = tr.find('th:first');
-      if (old_th.get(0) != th.get(0)) { // don't move self to self
-        old_th.before(th);
-        old_th.before(td);
+header_layout.order_fields = function (new_fields) {
+  var this_ = this;
+  var cells = {};
+  var headers = {};
+  var fullrows = {};
+  jQuery.each(new_fields, function(idx, name) {
+    var header = this_.get_tx(name);
+    var cell = header.next('td');
+    headers[name] = header;
+    cells[name] = cell;
+    fullrows[name] = cell.attr('colspan') === '3';
+  });
+  var groups = dynfields_group(new_fields, 2,
+                               function(name) { return fullrows[name] });
+  var tbody = jQuery('#ticket table.properties > tbody');
+  var target_row;
+  jQuery.each(groups, function(idx, group) {
+    var col1 = group[0];
+    var col2 = group[1];
+    var row = jQuery('<tr>').append(headers[col1], cells[col1]);
+    if (col2 !== true) {
+      var header2, cell2;
+      if (col2) {
+        header2 = headers[col2];
+        cell2 = cells[col2];
+      } else {
+        cell2 = jQuery('<td>');
+        header2 = jQuery('<th>');
       }
+      row.append(header2, cell2);
+    }
+    if (target_row === undefined) {
+      tbody.prepend(row);
     } else {
-      var old_td = tr.find('td:last');
-      if (old_td.get(0) != td.get(0)) { // don't move self to self
-        old_td.after(td);
-        old_td.after(th);
-      }
+      target_row.after(row);
     }
-  } else {
-    // no columns so just insert
-    tr.append(th);
-    tr.append(td);
-  }
+    target_row = row;
+  });
+  tbody.children('tr').each(function() {
+    var row = $(this);
+    var headers = row.children('th');
+    if (headers.length === 0 || !jQuery.trim(headers.text()))
+      row.remove();
+  });
 };
diff --git a/dynfields/htdocs/rules.js b/dynfields/htdocs/rules.js
index ad03dbe09..7b5a080bd 100755
--- a/dynfields/htdocs/rules.js
+++ b/dynfields/htdocs/rules.js
@@ -22,7 +22,7 @@ var clearrule = new Rule('ClearRule'); // must match python class name exactly
 
 // apply
 clearrule.apply = function (input, spec) {
-  if (input.attr('id').slice(6) !== spec.trigger)
+  if (input[0].name.slice(6) !== spec.trigger)
     return;
 
   var target = spec.target;
@@ -92,7 +92,7 @@ var defaultrule = new Rule('DefaultRule'); // must match python class name exact
 
 // apply
 defaultrule.apply = function (input, spec) {
-  if (input.attr('id').slice(6) !== spec.trigger)
+  if (input[0].name.slice(6) !== spec.trigger)
     return;
 
   var $field = jQuery(get_selector(spec.target));
@@ -150,11 +150,12 @@ var hiderule = new Rule('HideRule'); // must match python class name exactly
 
 // setup
 hiderule.setup = function (input, spec) {
-  var id = input.attr('id');
-  if (id) { // no input fields when on /query page
+  var name = input[0].name;
+  if (name) { // no input fields when on /query page
     // show and reset elements controlled by this input field
-    var trigger = id.substring(6); // ids start with 'field-'
-    jQuery('.dynfields-' + trigger)
+    var trigger = name.substring(6); // ids start with 'field-'
+    jQuery('#properties, #ticket .properties')
+      .find('.dynfields-' + trigger)
       .removeClass('dynfields-hide dynfields-' + trigger)
       .show();
   }
@@ -162,34 +163,26 @@ hiderule.setup = function (input, spec) {
 
 // apply
 hiderule.apply = function (input, spec) {
+  var input_0 = input[0];
   var trigger = spec.trigger;
+  if (input_0.name.slice(6) !== spec.trigger)
+    return;
   var target = spec.target;
-
-  // hide field in the header if cleared or always hidden
   var clear_on_hide = spec.clear_on_hide.toLowerCase() == 'true';
   var hide_always = spec.hide_always.toLowerCase() == 'true'
-  if (clear_on_hide || hide_always) {
-    th = jQuery('#h_' + spec.target);
-    td = th.next('td');
-    td.addClass('dynfields-hide dynfields-' + trigger);
-    th.addClass('dynfields-hide dynfields-' + trigger);
-  }
-
-  if (input.attr('id').slice(6) !== spec.trigger)
-    return;
 
   // process hide rule
   var v;
-  if (input.attr('type') == 'checkbox')
+  if (input_0.type === 'checkbox')
     v = (input.is(':checked')) ? "1" : "0";
   else
     v = input.val();
   var l = spec.trigger_value.split('|'); // supports list of trigger values
-  if ((jQuery.inArray(v, l) != -1 && spec.op == 'hide') ||
-    (jQuery.inArray(v, l) == -1 && spec.op == 'show')) {
-
+  var rv = jQuery.inArray(v, l);
+  if (rv !== -1 && spec.op === 'hide' || rv === -1 && spec.op === 'show') {
     // we want to hide the input fields's td and related th
-    var field = jQuery('#field-' + target + ',input[name="field_' + target + '"]');
+    var field = jQuery('#field-@, #properties input[name="field_@"]'
+                       .replace(/@/g, target));
     var td = field.closest('td');
     var th = td.prev('th');
     var cls = 'dynfields-hide dynfields-' + trigger;
@@ -199,8 +192,7 @@ hiderule.apply = function (input, spec) {
     th.addClass(cls);
 
     // let's also clear out the field's value to avoid confusion
-    if (spec.clear_on_hide.toLowerCase() == 'true' &&
-      field.val() && field.val().length) { // Chrome fix - see #8654
+    if (clear_on_hide && field.val() && field.val().length) { // Chrome fix - see #8654
       if (field.attr('type') == 'checkbox') {
         if (field.is(':checked')) {
           field.removeAttr('checked').change();
@@ -213,12 +205,20 @@ hiderule.apply = function (input, spec) {
           field.change(); // cascade rules
       }
     }
+    // hide field in the header if cleared or always hidden
+    if (clear_on_hide || hide_always) {
+      var th = jQuery('#h_' + target);
+      var td = th.next('td');
+      td.addClass('dynfields-hide dynfields-' + trigger);
+      th.addClass('dynfields-hide dynfields-' + trigger);
+    }
   }
 };
 
 // complete
 hiderule.complete = function (input, spec) {
-  jQuery('.dynfields-hide').hide();
+  jQuery('#properties .dynfields-hide, ' +
+         '#ticket .properties .dynfields-hide').hide();
 
   // update layout (see layout.js)
   inputs_layout.update(spec);
@@ -232,7 +232,7 @@ hiderule.complete = function (input, spec) {
         '    <a href="#no3" onClick="jQuery(\'.dynfields-link\').show(); jQuery(\'#dynfields-show-link\').remove();">Show hidden fields</a>' +
         '  </td>' +
         '</tr>';
-      jQuery('.dynfields-link:last').parents('tbody:first').append(html);
+      jQuery('.dynfields-link:last').closest('tbody').append(html);
     }
   }
 };
@@ -273,7 +273,7 @@ var validaterule = new Rule('ValidateRule'); // must match python class name exa
 validaterule.setup = function (input, spec) {
   var field = jQuery('#field-' + spec.target);
   var submit = jQuery('input[name=submit]');
-  var form = submit.parents('form');
+  var form = submit.closest('form');
 
   // reset "alert only once per form submission"
   submit.click(function () {
