From 94703e23a117c58a34df0966b267d0da419fc3d4 Mon Sep 17 00:00:00 2001
From: Chris Hallberg <crhallberg@gmail.com>
Date: Thu, 15 Dec 2016 15:42:53 -0500
Subject: [PATCH] Form attribute support (#802)

- Use form="id" attribute to simplify form markup
- Includes polyfill for legacy browsers
---
 .../VuFind/src/VuFind/Service/ReCaptcha.php   |  2 +-
 .../src/VuFind/View/Helper/Root/Record.php    |  6 +-
 .../View/Helper/Root/RecordTest.php           |  4 +-
 themes/bootstrap3/js/cart.js                  | 84 ++++++++++---------
 themes/bootstrap3/js/common.js                | 26 +++++-
 .../bootstrap3/js/lib/form-attr-polyfill.js   | 68 +++++++++++++++
 themes/bootstrap3/js/lightbox.js              | 11 +--
 themes/bootstrap3/js/record.js                | 19 ++---
 .../templates/RecordTab/usercomments.phtml    |  2 -
 .../templates/record/checkbox.phtml           |  4 +-
 .../search/bulk-action-buttons.phtml          | 12 +--
 .../templates/search/list-list.phtml          |  4 +-
 .../bootstrap3/templates/search/results.phtml |  8 +-
 themes/bootstrap3/theme.config.php            |  1 +
 14 files changed, 171 insertions(+), 80 deletions(-)
 create mode 100644 themes/bootstrap3/js/lib/form-attr-polyfill.js

diff --git a/module/VuFind/src/VuFind/Service/ReCaptcha.php b/module/VuFind/src/VuFind/Service/ReCaptcha.php
index 2a8ff092584..a8152779436 100644
--- a/module/VuFind/src/VuFind/Service/ReCaptcha.php
+++ b/module/VuFind/src/VuFind/Service/ReCaptcha.php
@@ -66,7 +66,7 @@ class ReCaptcha extends \LosReCaptcha\Service\ReCaptcha
         $divregex = '/<div[^>]*id=[\'"]recaptcha_widget[\'"][^>]*>/';
 
         $scriptRegex = '|<script[^>]*></script>|';
-        $scriptReplacement = '<script>/*form magic*/</script>';
+        $scriptReplacement = ''; // remove
 
         return preg_replace(
             [$divregex, $scriptRegex],
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Record.php b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
index 1054069909e..7dd53c668dd 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Record.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
@@ -410,16 +410,20 @@ class Record extends AbstractHelper
      * Render an HTML checkbox control for the current record.
      *
      * @param string $idPrefix Prefix for checkbox HTML ids
+     * @param string $formAttr ID of form for [form] attribute
      *
      * @return string
      */
-    public function getCheckbox($idPrefix = '')
+    public function getCheckbox($idPrefix = '', $formAttr = false)
     {
         static $checkboxCount = 0;
         $id = $this->driver->getSourceIdentifier() . '|'
             . $this->driver->getUniqueId();
         $context
             = ['id' => $id, 'count' => $checkboxCount++, 'prefix' => $idPrefix];
+        if ($formAttr) {
+            $context['formAttr'] = $formAttr;
+        }
         return $this->contextHelper->renderInContext(
             'record/checkbox.phtml', $context
         );
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
index 32434ba4980..33121548b7d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
@@ -271,10 +271,10 @@ class RecordTest extends \PHPUnit_Framework_TestCase
     {
         $context = $this->getMockContext();
         $context->expects($this->at(1))->method('renderInContext')
-            ->with($this->equalTo('record/checkbox.phtml'), $this->equalTo(['id' => 'Solr|000105196', 'count' => 0, 'prefix' => 'bar']))
+            ->with($this->equalTo('record/checkbox.phtml'), $this->equalTo(['id' => 'Solr|000105196', 'count' => 0, 'prefix' => 'bar', 'formAttr' => 'foo']))
             ->will($this->returnValue('success'));
         $context->expects($this->at(2))->method('renderInContext')
-            ->with($this->equalTo('record/checkbox.phtml'), $this->equalTo(['id' => 'Solr|000105196', 'count' => 1, 'prefix' => 'bar']))
+            ->with($this->equalTo('record/checkbox.phtml'), $this->equalTo(['id' => 'Solr|000105196', 'count' => 1, 'prefix' => 'bar', 'formAttr' => 'foo']))
             ->will($this->returnValue('success'));
         $record = $this->getRecord(
             $this->loadRecordFixture('testbug1.json'), [], $context
diff --git a/themes/bootstrap3/js/cart.js b/themes/bootstrap3/js/cart.js
index 5cd0bdeb604..3d2474241b6 100644
--- a/themes/bootstrap3/js/cart.js
+++ b/themes/bootstrap3/js/cart.js
@@ -122,47 +122,52 @@ VuFind.register('cart', function Cart() {
   }
 
   var _cartNotificationTimeout = false;
-  function _registerUpdate($form) {
-    if ($form) {
-      $("#updateCart, #bottom_updateCart").unbind('click').click(function cartUpdate() {
-        var elId = this.id;
-        var selectedBoxes = $("input[name='ids[]']:checked", $form);
-        var selected = [];
-        $(selectedBoxes).each(function cartCheckboxValues(i) {
-          selected[i] = this.value;
+  function _registerUpdate(_form) {
+    var $form = typeof _form === 'undefined'
+      ? $('form[name="bulkActionForm"]')
+      : $(_form);
+    $("#updateCart, #bottom_updateCart").unbind('click').click(function cartUpdate() {
+      var elId = this.id;
+      var selected = [];
+      var selectedInForm = $form.find('input[name="ids[]"]:checked');
+      var selectedFormAttr = $('input[form="' + $form.attr('id') + '"][name="ids[]"]:checked');
+      $(selectedInForm).each(function cartFormCheckboxValues() {
+        selected.push(this.value);
+      });
+      $(selectedFormAttr).each(function cartAttrCheckboxValues() {
+        selected.push(this.value);
+      });
+      if (selected.length > 0) {
+        var msg = "";
+        var orig = getFullItems();
+        $(selected).each(function cartCheckedItemsAdd() {
+          var data = this.split('|');
+          addItem(data[1], data[0]);
         });
-        if (selected.length > 0) {
-          var msg = "";
-          var orig = getFullItems();
-          $(selected).each(function cartCheckedItemsAdd() {
-            var data = this.split('|');
-            addItem(data[1], data[0]);
-          });
-          var updated = getFullItems();
-          var added = updated.length - orig.length;
-          var inCart = selected.length - added;
-          msg += added + " " + VuFind.translate('itemsAddBag');
-          if (updated.length >= parseInt(VuFind.translate('bookbagMax'), 10)) {
-            msg += "<br/>" + VuFind.translate('bookbagFull');
-          }
-          if (inCart > 0 && orig.length > 0) {
-            msg += "<br/>" + inCart + " " + VuFind.translate('itemsInBag');
-          }
-          $('#' + elId).data('bs.popover').options.content = msg;
-          $('#cartItems strong').html(updated.length);
-        } else {
-          $('#' + elId).data('bs.popover').options.content = VuFind.translate('bulk_noitems_advice');
+        var updated = getFullItems();
+        var added = updated.length - orig.length;
+        var inCart = selected.length - added;
+        msg += added + " " + VuFind.translate('itemsAddBag');
+        if (updated.length >= parseInt(VuFind.translate('bookbagMax'), 10)) {
+          msg += "<br/>" + VuFind.translate('bookbagFull');
         }
-        $('#' + elId).popover('show');
-        if (_cartNotificationTimeout !== false) {
-          clearTimeout(_cartNotificationTimeout);
+        if (inCart > 0 && orig.length > 0) {
+          msg += "<br/>" + inCart + " " + VuFind.translate('itemsInBag');
         }
-        _cartNotificationTimeout = setTimeout(function notificationHide() {
-          $('#' + elId).popover('hide');
-        }, 5000);
-        return false;
-      });
-    }
+        $('#' + elId).data('bs.popover').options.content = msg;
+        $('#cartItems strong').html(updated.length);
+      } else {
+        $('#' + elId).data('bs.popover').options.content = VuFind.translate('bulk_noitems_advice');
+      }
+      $('#' + elId).popover('show');
+      if (_cartNotificationTimeout !== false) {
+        clearTimeout(_cartNotificationTimeout);
+      }
+      _cartNotificationTimeout = setTimeout(function notificationHide() {
+        $('#' + elId).popover('hide');
+      }, 5000);
+      return false;
+    });
   }
 
   function init() {
@@ -192,8 +197,7 @@ VuFind.register('cart', function Cart() {
       });
     } else {
       // Search results
-      var $form = $('form[name="bulkActionForm"]');
-      _registerUpdate($form);
+      _registerUpdate();
     }
     $("#updateCart, #bottom_updateCart").popover({content: '', html: true, trigger: 'manual'});
     updateCount();
diff --git a/themes/bootstrap3/js/common.js b/themes/bootstrap3/js/common.js
index 1b4551ecf1e..fcd3fe6fb54 100644
--- a/themes/bootstrap3/js/common.js
+++ b/themes/bootstrap3/js/common.js
@@ -1,5 +1,5 @@
 /*global grecaptcha, isPhoneNumberValid */
-/*exported VuFind, htmlEncode, deparam, moreFacets, lessFacets, phoneNumberFormHandler, recaptchaOnLoad, bulkFormHandler */
+/*exported VuFind, htmlEncode, deparam, moreFacets, lessFacets, phoneNumberFormHandler, recaptchaOnLoad, resetCaptcha, bulkFormHandler */
 
 // IE 9< console polyfill
 window.console = window.console || {log: function polyfillLog() {}};
@@ -170,6 +170,14 @@ function recaptchaOnLoad() {
     }
   }
 }
+function resetCaptcha($form) {
+  if (typeof grecaptcha !== 'undefined') {
+    var captcha = $form.find('.g-recaptcha');
+    if (captcha.length > 0) {
+      grecaptcha.reset(captcha.data('captchaId'));
+    }
+  }
+}
 
 function bulkFormHandler(event, data) {
   if ($('.checkbox-select-item:checked,checkbox-select-all:checked').length === 0) {
@@ -310,7 +318,7 @@ function setupFacets() {
           $(item).collapse('hide');
         }
       } finally {
-        $.support.transition = saveTransition;    
+        $.support.transition = saveTransition;
       }
     }
   });
@@ -343,10 +351,20 @@ $(document).ready(function commonDocReady() {
 
   // Checkbox select all
   $('.checkbox-select-all').change(function selectAllCheckboxes() {
-    $(this).closest('form').find('.checkbox-select-item').prop('checked', this.checked);
+    var $form = $(this).closest('form')
+    $form.find('.checkbox-select-item').prop('checked', this.checked);
+    $('[form="' + $form.attr('id') + '"]').prop('checked', this.checked);
   });
   $('.checkbox-select-item').change(function selectAllDisable() {
-    $(this).closest('form').find('.checkbox-select-all').prop('checked', false);
+    var $form = $(this).closest('form');
+    if ($form.length === 0 && this.form) {
+      $form = $(this.form);
+    }
+    if ($form.length === 0) {
+      return;
+    }
+    $form.find('.checkbox-select-all').prop('checked', false);
+    $('.checkbox-select-all[form="' + $form.attr('id') + '"]').prop('checked', false);
   });
 
   // handle QR code links
diff --git a/themes/bootstrap3/js/lib/form-attr-polyfill.js b/themes/bootstrap3/js/lib/form-attr-polyfill.js
new file mode 100644
index 00000000000..000a63e44a5
--- /dev/null
+++ b/themes/bootstrap3/js/lib/form-attr-polyfill.js
@@ -0,0 +1,68 @@
+/**
+ * From http://stackoverflow.com/questions/17742275/polyfill-html5-input-form-attribute/26696165#26696165
+ * Recommended by https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
+ * Adapted to eslint styling and updated detection by Chris Hallberg (@crhallberg) for VuFind
+ */
+(function formAttrPolyfill($) {
+  /**
+   * polyfill for html5 form attr
+   */
+
+  // every browser supports except IE
+  if (typeof document.documentMode == 'undefined') {
+    // any other browser? skip
+    return;
+  }
+
+  function resetFormAttr(form) {
+    var $form = $(form);
+    $form.find('.js-form-attr').remove();
+    return $form;
+  }
+  function makeFieldElement(data) {
+    return $('<input/>', data)
+      .addClass('js-form-attr')
+      .attr('type', 'hidden');
+  }
+
+  $(document).ready(function formAttrReady() {
+    /**
+     * Find all input fields with form attribute point to jQuery object
+     *
+     */
+    $('form[id]').submit(function locateFormAttr(/*e*/) {
+      // serialize data
+      var data = $('[form=' + this.id + ']').serializeArray();
+      // append data to form
+      var $form = resetFormAttr(this);
+      for (var i=0; i<data.length; i++) {
+        $form.append(makeFieldElement(data[i]));
+      }
+      return true;
+    }).each(function locateFormAttrEach() {
+      var form = this,
+        $fields = $('[form=' + this.id + ']');
+
+      $fields.filter('button, input').filter('[type=reset],[type=submit]').click(function formAttrButtonSubmit() {
+        var type = this.type.toLowerCase();
+        if (type === 'reset') {
+          // reset form
+          form.reset();
+          // for elements outside form
+          $fields.each(function formAttrGatherFieldData() {
+            this.value = this.defaultValue;
+            this.checked = this.defaultChecked;
+          }).filter('select').each(function formAttrGatherSelectData() {
+            $(this).find('option').each(function formAttrGatherOptionData() {
+              this.selected = this.defaultSelected;
+            });
+          });
+        } else if (type.match(/^submit|image$/i)) {
+          var $form = resetFormAttr(form);
+          $form.append( makeFieldElement({name: this.name, value: this.value}) ).submit();
+        }
+      });
+    });
+  });
+
+})(jQuery);
diff --git a/themes/bootstrap3/js/lightbox.js b/themes/bootstrap3/js/lightbox.js
index 37a5a9352b1..a1e11f4b65a 100644
--- a/themes/bootstrap3/js/lightbox.js
+++ b/themes/bootstrap3/js/lightbox.js
@@ -1,4 +1,4 @@
-/*global grecaptcha, recaptchaOnLoad, VuFind */
+/*global grecaptcha, recaptchaOnLoad, resetCaptcha, VuFind */
 VuFind.register('lightbox', function Lightbox() {
   // State
   var _originalUrl = false;
@@ -279,9 +279,7 @@ VuFind.register('lightbox', function Lightbox() {
       method: $(form).attr('method') || 'GET',
       data: data
     }).done(function recaptchaReset() {
-      if (typeof grecaptcha !== 'undefined') {
-        grecaptcha.reset($(form).find('.g-recaptcha').data('captchaId'));
-      }
+      resetCaptcha($(form));
     });
 
     VuFind.modal('show');
@@ -301,7 +299,10 @@ VuFind.register('lightbox', function Lightbox() {
     // Handle submit buttons attached to a form as well as those in a form. Store
     // information about which button was clicked here as checking focused button
     // doesn't work on all browsers and platforms.
-    $('form[data-lightbox] [type=submit]').click(_storeClickedStatus);
+    $('form[data-lightbox]').each(function bindFormSubmitsLightbox(i, form) {
+      $(form).find('[type=submit]').click(_storeClickedStatus);
+      $('[type="submit"][form="' + form.id + '"]').click(_storeClickedStatus);
+    });
   }
 
   function reset() {
diff --git a/themes/bootstrap3/js/record.js b/themes/bootstrap3/js/record.js
index e090e917889..63900a8571f 100644
--- a/themes/bootstrap3/js/record.js
+++ b/themes/bootstrap3/js/record.js
@@ -1,4 +1,4 @@
-/*global deparam, grecaptcha, recaptchaOnLoad, syn_get_widget, userIsLoggedIn, VuFind */
+/*global deparam, grecaptcha, recaptchaOnLoad, resetCaptcha, syn_get_widget, userIsLoggedIn, VuFind */
 /*exported ajaxTagUpdate, recordDocReady */
 
 /**
@@ -78,9 +78,7 @@ function refreshCommentList($target, recordId, recordSource) {
       return false;
     });
     $target.find('.comment-form input[type="submit"]').button('reset');
-    if (typeof grecaptcha !== 'undefined') {
-      grecaptcha.reset();
-    }
+    resetCaptcha($target);
   });
 }
 
@@ -109,16 +107,15 @@ function registerAjaxCommentRecord() {
       dataType: 'json'
     })
     .done(function addCommentDone(/*response, textStatus*/) {
-      var $tab = $(form).closest('.list-tab-content');
+      var $form = $(form);
+      var $tab = $form.closest('.list-tab-content');
       if (!$tab.length) {
-        $tab = $(form).closest('.tab-pane');
+        $tab = $form.closest('.tab-pane');
       }
       refreshCommentList($tab, id, recordSource);
-      $(form).find('textarea[name="comment"]').val('');
-      $(form).find('input[type="submit"]').button('loading');
-      if (typeof grecaptcha !== 'undefined') {
-        grecaptcha.reset($(form).find('.g-recaptcha').data('captchaId'));
-      }
+      $form.find('textarea[name="comment"]').val('');
+      $form.find('input[type="submit"]').button('loading');
+      resetCaptcha($form);
     })
     .fail(function addCommentFail(response, textStatus) {
       if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') { return; }
diff --git a/themes/bootstrap3/templates/RecordTab/usercomments.phtml b/themes/bootstrap3/templates/RecordTab/usercomments.phtml
index e00d5eaaffc..4bd7cb6e88b 100644
--- a/themes/bootstrap3/templates/RecordTab/usercomments.phtml
+++ b/themes/bootstrap3/templates/RecordTab/usercomments.phtml
@@ -19,8 +19,6 @@
         <textarea name="comment" class="form-control" rows="3" required></textarea><br/>
         <? if ($this->tab->isRecaptchaActive()): ?>
           <?=$this->recaptcha()->html(true, false) ?><br/>
-        <? else: ?>
-          <script>/* workaround for nested form bug */</script>
         <? endif; ?>
         <input class="btn btn-primary" data-loading-text="<?=$this->transEsc('Submitting') ?>..." type="submit" value="<?=$this->transEsc("Add your comment")?>"/>
       <? else: ?>
diff --git a/themes/bootstrap3/templates/record/checkbox.phtml b/themes/bootstrap3/templates/record/checkbox.phtml
index e6cba3addf1..f889038dbba 100644
--- a/themes/bootstrap3/templates/record/checkbox.phtml
+++ b/themes/bootstrap3/templates/record/checkbox.phtml
@@ -1,2 +1,2 @@
-<input class="checkbox-select-item" type="checkbox" name="ids[]" value="<?=$this->escapeHtmlAttr($this->id) ?>"/>
-<input type="hidden" name="idsAll[]" value="<?=$this->escapeHtmlAttr($this->id) ?>"/>
+<input class="checkbox-select-item" type="checkbox" name="ids[]" value="<?=$this->escapeHtmlAttr($this->id) ?>"<? if(isset($this->formAttr)): ?> form="<?=$this->formAttr ?>"<? endif; ?>/>
+<input type="hidden" name="idsAll[]" value="<?=$this->escapeHtmlAttr($this->id) ?>"<? if(isset($this->formAttr)): ?> form="<?=$this->formAttr ?>"<? endif; ?>/>
diff --git a/themes/bootstrap3/templates/search/bulk-action-buttons.phtml b/themes/bootstrap3/templates/search/bulk-action-buttons.phtml
index 7122165f7c6..9c3edeca19b 100644
--- a/themes/bootstrap3/templates/search/bulk-action-buttons.phtml
+++ b/themes/bootstrap3/templates/search/bulk-action-buttons.phtml
@@ -3,23 +3,23 @@
   <div class="bulkActionButtons hidden-print">
     <div class="checkbox">
       <label>
-        <input type="checkbox" class="checkbox-select-all" name="selectAll" id="<?=$this->idPrefix?>addFormCheckboxSelectAll"/> <?=$this->transEsc('select_page')?>
+        <input type="checkbox" class="checkbox-select-all" name="selectAll" id="<?=$this->idPrefix?>addFormCheckboxSelectAll"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/> <?=$this->transEsc('select_page')?>
         &#124; <?=$this->transEsc('with_selected')?>:
       </label>
     </div>
     <div class="btn-group">
       <? if (isset($this->showBulkOptions) && $this->showBulkOptions): ?>
-        <input id="ribbon-email" class="btn btn-default" type="submit" name="email" title="<?=$this->transEsc('bookbag_email_selected')?>" value="<?=$this->transEsc('Email')?>"/>
+        <input id="ribbon-email" class="btn btn-default" type="submit" name="email" title="<?=$this->transEsc('bookbag_email_selected')?>" value="<?=$this->transEsc('Email')?>"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/>
         <? $exportOptions = $this->export()->getBulkOptions(); if (count($exportOptions) > 0): ?>
-          <input id="ribbon-export" class="btn btn-default" type="submit" name="export" title="<?=$this->transEsc('bookbag_export_selected')?>" value="<?=$this->transEsc('Export')?>"/>
+          <input id="ribbon-export" class="btn btn-default" type="submit" name="export" title="<?=$this->transEsc('bookbag_export_selected')?>" value="<?=$this->transEsc('Export')?>"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/>
         <? endif; ?>
-        <input id="ribbon-print" class="btn btn-default" type="submit" name="print" title="<?=$this->transEsc('bookbag_print_selected')?>" value="<?=$this->transEsc('Print')?>"/>
+        <input id="ribbon-print" class="btn btn-default" type="submit" name="print" title="<?=$this->transEsc('bookbag_print_selected')?>" value="<?=$this->transEsc('Print')?>"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/>
         <? if ($this->userlist()->getMode() !== 'disabled'): ?>
-          <input id="ribbon-save" class="btn btn-default" type="submit" name="saveCart" title="<?=$this->transEsc('bookbag_save_selected')?>" value="<?=$this->transEsc('Save')?>"/>
+          <input id="ribbon-save" class="btn btn-default" type="submit" name="saveCart" title="<?=$this->transEsc('bookbag_save_selected')?>" value="<?=$this->transEsc('Save')?>"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/>
         <? endif; ?>
       <? endif; ?>
       <? if (isset($this->showCartControls) && $this->showCartControls): ?>
-        <input id="<?=$this->idPrefix?>updateCart" type="submit" class="btn btn-default" name="add" value="<?=$this->transEsc('Add to Book Bag')?>"/>
+        <input id="<?=$this->idPrefix?>updateCart" type="submit" class="btn btn-default" name="add" value="<?=$this->transEsc('Add to Book Bag')?>"<?if($this->formAttr):?> form="<?=$this->escapeHtmlAttr($this->formAttr) ?>"<? endif; ?>/>
       <? endif; ?>
     </div>
   </div>
diff --git a/themes/bootstrap3/templates/search/list-list.phtml b/themes/bootstrap3/templates/search/list-list.phtml
index b6adf0cd9e9..a01cfdd9541 100644
--- a/themes/bootstrap3/templates/search/list-list.phtml
+++ b/themes/bootstrap3/templates/search/list-list.phtml
@@ -7,7 +7,7 @@
     <div class="checkbox hidden-print">
       <? if ($showCheckboxes): ?>
         <label>
-          <?=$this->record($current)->getCheckbox()?>
+          <?=$this->record($current)->getCheckbox('', 'search-cart-form')?>
           <?=$recordNumber ?>
         </label>
       <? else: ?>
@@ -16,4 +16,4 @@
     </div>
     <?=$this->record($current)->getSearchResult('list')?>
   </div>
-<? endforeach; ?>
\ No newline at end of file
+<? endforeach; ?>
diff --git a/themes/bootstrap3/templates/search/results.phtml b/themes/bootstrap3/templates/search/results.phtml
index 36b8ebffcfe..77e3a907557 100644
--- a/themes/bootstrap3/templates/search/results.phtml
+++ b/themes/bootstrap3/templates/search/results.phtml
@@ -109,12 +109,12 @@
         <? endif; ?>
       <? endforeach; ?>
     <? else: ?>
-      <form class="form-inline" method="post" name="bulkActionForm" action="<?=$this->url('cart-searchresultsbulk')?>" data-lightbox data-lightbox-onsubmit="bulkFormHandler">
+      <form id="search-cart-form" class="form-inline" method="post" name="bulkActionForm" action="<?=$this->url('cart-searchresultsbulk')?>" data-lightbox data-lightbox-onsubmit="bulkFormHandler">
         <?=$this->context($this)->renderInContext('search/bulk-action-buttons.phtml', ['idPrefix' => ''])?>
-        <?=$this->render('search/list-' . $this->params->getView() . '.phtml')?>
-        <?=$this->context($this)->renderInContext('search/bulk-action-buttons.phtml', ['idPrefix' => 'bottom_'])?>
-        <?=$this->paginationControl($this->results->getPaginator(), 'Sliding', 'search/pagination.phtml', ['results' => $this->results, 'options' => isset($this->paginationOptions) ? $this->paginationOptions : []])?>
       </form>
+      <?=$this->render('search/list-' . $this->params->getView() . '.phtml')?>
+      <?=$this->context($this)->renderInContext('search/bulk-action-buttons.phtml', ['idPrefix' => 'bottom_', 'formAttr' => 'search-cart-form'])?>
+      <?=$this->paginationControl($this->results->getPaginator(), 'Sliding', 'search/pagination.phtml', ['results' => $this->results, 'options' => isset($this->paginationOptions) ? $this->paginationOptions : []])?>
 
       <div class="searchtools hidden-print">
         <strong><?=$this->transEsc('Search Tools')?>:</strong>
diff --git a/themes/bootstrap3/theme.config.php b/themes/bootstrap3/theme.config.php
index 45b2f9d805e..37804a60d1e 100644
--- a/themes/bootstrap3/theme.config.php
+++ b/themes/bootstrap3/theme.config.php
@@ -15,6 +15,7 @@ return array(
         'vendor/bootstrap.min.js',
         'vendor/bootstrap-accessibility.min.js',
         'vendor/validator.min.js',
+        'lib/form-attr-polyfill.js', // input[form] polyfill, cannot load conditionally, since we need all versions of IE
         'lib/autocomplete.js',
         'common.js',
         'lightbox.js',
-- 
GitLab