Search code examples
sortingselectfilterjquery-isotope

Isotope Filter, Sort, Search and Url Hash


I'm using Isotope to sort, filter and search a page of products. So far so good. I'm stuck on a couple of areas. I've not been able to find an example online with the exact combination of features I require so need some help.

In a nutshell I have multiple select dropdowns filtering products, select price sort order and a quick search input field. All work but I have some needed amends.

TO DO:

Search doesn't work after filtering. I need it to work with the filters.

Addition of sort by 'Sale' and 'New In' on the price select dropdown.

Addition of URL hash listener to create links for filtering i.e link to New In sorted first.

Isotope website

Select Example

URL Hash example

Quick search example

My current JS for filtering and sorting:

        $(document).ready(function(){

// quick search regex
var qsRegex;
var filterValue;
// init Isotope
var $grid = $(".grid").isotope({
  itemSelector: ".grid-item",
  layoutMode: "fitRows",
  getSortData: {
    price: '.t-price parseInt',
    category: '[data-category]',
  },
  filter: function() {
    var $this = $(this);
    var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
    var selectResult = filterValue ? $this.is(filterValue) : true;
    return searchResult  && selectResult;
  }
});

      // bind filter on select change
//$(".filter-select").on("change", function() {
       // get filter value from option value
 // filterValue = $(this).val();
  //console.log(filterValue);
  //$grid.isotope();
//});


// store filter for each group
var filters = {};

$('.filter-select').on( 'change', function( event ) {
  var $select = $( event.target );
  // get group key
  var filterGroup = $select.attr('value-group');
  // set filter for group
  filters[ filterGroup ] = event.target.value;
  // combine filters
  var filterValue = concatValues( filters );
  // set filter for Isotope
  $grid.isotope({ filter: filterValue });
});

// flatten object by concatting values
function concatValues( obj ) {
  var value = '';
  for ( var prop in obj ) {
    value += obj[ prop ];
  }
  return value;
}



$('#price-sort').on( 'change', function() {
          var type = $(this).find(':selected').attr('data-sorttype');
          console.log(type);
    var sortValue = this.value;
      if(type=='ass'){$grid.isotope({ sortBy: sortValue , sortAscending: false});}
          else{$grid.isotope({ sortBy: sortValue , sortAscending: true});}
   $grid.isotope({ sortBy: sortValue });
  });


  // change is-checked class on buttons
  $('#price-sort').on( 'change', function() {
    var sortByValue = this.value;
      console.log(sortByValue);
    $grid.isotope({ sortBy: sortByValue});
  });


// use value of search field to filter
var $quicksearch = $(".quicksearch").keyup(
  debounce(function() {
    qsRegex = new RegExp($quicksearch.val(), "gi");
    $grid.isotope();
  })
);
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
  var timeout;
  return function debounced() {
    if (timeout) {
      clearTimeout(timeout);
    }
    function delayed() {
      fn();
      timeout = null;
    }
    setTimeout(delayed, threshold || 100);
  };
}


  });

HTML:

    <div id="sort-filter">
        <div id="sort">
                        <select id="price-sort" class="select-css form-control long">
                <option selected disabled class="s-title"> Sort </option>
                <option data-sorttype="dec" value="price">£ Low To High</option>
                <option data-sorttype="ass" value="price">£ High To Low</option>

            </select>

        </div>
        <div class="filters">
                    <select class="filter-select select-css short" value-group="sizes" id="sizes">
                    <option selected disabled class="s-title"> Size </option>
                      <option value="*">All</option>
                      <option value=".XS">XS</option>
                      <option value=".S">S</option>
                      <option value=".M">M</option>
                      <option value=".L">L</option>
                      <option value=".XL">XL</option>
                      <option value=".XXL">XXL</option>
                    </select>
    </div>
    </div>

<div class="container">

    <ul class="grid cs-style-3">
        <div class="grid-sizer"></div>

            <li class="grid-item XS Male Beige Bags Mint">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="hat sale item">
                </figure>
                <div id="pro-deets">
                <h3>hat sale item</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">3</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item L Female Brown Tops Worn">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="product no sale no new">
                </figure>
                <div id="pro-deets">
                <h3>product no sale no new</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">40</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item L Female Brown Tops Worn New" data-category="New">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="Skirt">
                </figure>
                <div id="pro-deets">
                <h3>Skirt</h3>
                        <span id="price" class="holderpage">
                            £<span class="price t-price">10</span>

                        </span>
                </div></a>
            </li>

            <li class="grid-item XS Male Beige Bags Mint Sale" data-category="Sale">
                <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
                    <figure style="background-image: URL(image.jpg);">
                        <img src="/image2.jpg" alt="Jacket">
                </figure>
                <div id="pro-deets">
                <h3>Jacket</h3>
                        <span id="price" class="holderpage">
                            £<span class="price sale">30</span>
                            <span class="price">£<span class="t-price">20</span></span>
                        </span>
                </div></a>
            </li>

        </ul>
        </div>  

Solution

  • Note that this is a two parts answer. Check for part 2 from a different answer.

    Answer : Part 1

    When I read your code, you were on the right track. For the reason of why search and filter doesn't work together in your code, the problem was that, when you initialize the $grid, you defined a filter function for the $grid. However, when there is a change with the select filter group, you redefined that filter by calling $grid.isotope({ filter: filterValue }). When you call $grid.isotope() with any configurable values, the $grid will take on those new configuration.

    Thus, the answer to you question is just to have two variables. One to store a filter value, and one to store the search value. Whenever $grid.isotope() is called, it will just use those two values for filtering.

    There is also another few issues with your code. You don't need to do price-sort twice. That only need to be done one. When it comes to HTML, classes, and ids, an id should only happen one in a page. That mean, you can't have two division with the same id. It probably will not break your page if it is something unimportant. However, that can break your page when it comes to manipulating your page programmatically. Besides that, the way you utilize the filter-select is meant for getting values from two button group. But it could be use for your case, and I guess you probably will need that in the future as beside sizes, there will probably be colors, etc... Also, when comparing string in JS for your price-sort. It is better to compare strings with === for equality. For comparing strings in JS you can refer to this Which equals operator (== vs ===) should be used in JavaScript comparisons?.

    For code design, you could do as that. I think the way where you put everything like that in document.ready() will make the code run faster.

    For the answer code, the routines are simple. When document is ready, all the events associated with the search field and the select field is initialized. After that, the filterWithHash() function is bind with to the onhashchange event. That function is then executed to initialize the grid while checking the URL for any associate hashes.

    For hashes in the URL, try "filter=" and "search=". They can be used together without any issues. You can also convert that function into be able to take not only hashes but the get parameters.

    There are also some comments in the code that probably can help you.

    <html>
    <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>
    
    <script>
    $(document).ready(function(){
    // Quick Search Regex
    var qsRegex;
    // Filter Value
    var filterValue;
    // Grid not initialize yet.
    var $grid; 
    
    // Since there is only one filter group and that is sizes. 
    // It isn't necessary to be done like this. 
    // Just grab the selected value everytime the sizes select is changed.
    // However, this was still left like this.
    $('.filter-select').on( 'change', function( event ) {
      // Hold the filter values here.
      var filters = {};
      var $select = $( event.target );
      // Get group key
      var filterGroup = $select.attr('value-group');
      // Set filter for group
      filters[ filterGroup ] = event.target.value;
      // Assign the filter value to the global filterValue
      filterValue = concatValues( filters );
      // Execute $grid.isotope() to update with current global filter value.
      $grid.isotope();
    });
    
    // flatten object by concatting values
    function concatValues( obj ) {
      var value = '';
      for ( var prop in obj ) {
        value += obj[ prop ];
      }
      return value;
    }
    
    // change is-checked class on buttons
    // Only need one price-sort not two
    $('#price-sort').on( 'change', function() {
        var type = $(this).find(':selected').attr('data-sorttype');
        // REMEMBER TO TAKE THE CONSOLE LOG OUT IN PRODUCTION
        console.log(type);
        var sortValue = this.value;
        if( type === 'asc' ) $grid.isotope({ sortBy: sortValue , sortAscending: false});
        else $grid.isotope({ sortBy: sortValue , sortAscending: true});
        $grid.isotope({ sortBy: sortValue });
    });
    
    // use value of search field to filter
    var $quicksearch = $("#quicksearch").keyup(
      debounce(function() {
        qsRegex = new RegExp($quicksearch.val(), "gi");
        // Every time qsRegex is update do $grid.isotope() to update
        // The filter with global filterValue and qsRegex
        $grid.isotope();
      })
    );
    // debounce so filtering doesn't happen every millisecond
    function debounce(fn, threshold) {
      var timeout;
      return function debounced() {
        if (timeout) {
          clearTimeout(timeout);
        }
        function delayed() {
          fn();
          timeout = null;
        }
        setTimeout(delayed, threshold || 100);
      };
    }
    
    function getHashFilter() {
      // get filter=filterName
      var matches = location.hash.match( /filter=([^&]+)/i );
      var hashFilter = matches && matches[1];
      return hashFilter && decodeURIComponent( hashFilter );
    }
    
    function getSearchFilter() {
      // get search=filterName
      var matches = location.hash.match( /search=([^&]+)/i );
      var searchFilter = matches && matches[1];
      return searchFilter && decodeURIComponent( searchFilter );
    }
    
    /*
     * This function below can be customize to utilize not just only hashes
     * but also "Get Requests"
     */
    function filterWithHash() {
      var hashFilter = getHashFilter();
      var searchFilter = getSearchFilter();
      if ( !searchFilter && !hashFilter && $grid ) {
        return;
      }
      
      // If hashFilter is there, utilize it.
      if ( hashFilter ) {
          var selectValue = $('select[id="sizes"]').find('option[value="'+ hashFilter +'"]');
          // Only search for a value if it is found within the select fields, else disregard it.
          if ( selectValue.length > 0 ){
              selectValue.prop('selected', 'selected');
              filterValue = hashFilter;
          }
      }
      // If searhFilter is there, utilize it.
      if ( searchFilter) {
        $('#quicksearch').val(searchFilter);
        qsRegex = new RegExp(searchFilter, "gi");
      }
      
      /* If $grid is not initialize, it will get initialize. 
       * This will only happen on first run.
       * One grid is initilized, everytime grid.isotope() is run
       * without any value, grid will be updated to what initilized below.
       * Thus, each later run of isotope(), the filter will look at both,
       * the searchResult and the qsRegex if they are available.
      */
      if ( !$grid ) {
        $grid = $(".grid").isotope({
                  itemSelector: ".grid-item",
                  layoutMode: "fitRows",
                  getSortData: {
                    price: '.t-price parseInt',
                    category: '[data-category]',
                  },
                  filter: function() {
                    var $this = $(this);
                    var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
                    var selectResult = filterValue ? $this.is(filterValue) : true;
                    return searchResult && selectResult;
                  }
                });
      } else $grid.isotope();
    }
    /* 
     * Trigger filter with hash to initialize grid
     * and also to check the url for hash.
     */
    filterWithHash();
    
    // Bind the filterWithHash function to the hashchange event.
    $(window).on( 'hashchange', filterWithHash );
    
    });
    </script>
    </head>
    <body>
    <p><input type="text" id="quicksearch" placeholder="Search" /></p>
    
    <div id="sort-filter">
      <!-- Short Div -->
      <div id="sort">
        <select id="price-sort" class="select-css form-control long">
          <option selected disabled class="s-title"> Sort </option>
          <option data-sorttype="des" value="price">£ Low To High</option>
          <option data-sorttype="asc" value="price">£ High To Low</option>
        </select>
      </div>
    
      <!-- Filter Div -->
      <div class="filters">
        <select class="filter-select select-css short" value-group="sizes" id="sizes">
          <option selected disabled class="s-title"> Size </option>
          <option value="*">All</option>
          <option value=".XS">XS</option>
          <option value=".S">S</option>
          <option value=".M">M</option>
          <option value=".L">L</option>
          <option value=".XL">XL</option>
          <option value=".XXL">XXL</option>
        </select>
      </div>
    </div>
    
    <div class="container">
      <ul class="grid cs-style-3">
        <div class="grid-sizer"></div>
        
          <li class="grid-item XS Male Beige Bags Mint">
            <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
              <figure style="background-image: URL(image.jpg);">
                <img src="/image2.jpg" alt="hat sale item">
              </figure>
              <div id="pro-deets"> <!-- This should not be id -->
                <h3>hat sale item</h3>
                <span id="price" class="holderpage"> <!-- This should not be id -->
                  £<span class="price t-price">3</span>
                </span>
              </div>
            </a>
          </li>
    
          <li class="grid-item L Female Brown Tops Worn">
            <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
              <figure style="background-image: URL(image.jpg);">
                <img src="/image2.jpg" alt="product no sale no new">
              </figure>
              <div id="pro-deets">
                <h3>product no sale no new</h3>
                <span id="price" class="holderpage">
                  £<span class="price t-price">40</span>
                </span>
              </div>
            </a>
          </li>
    
          <li class="grid-item L Female Brown Tops Worn New" data-category="New">
            <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
              <figure style="background-image: URL(image.jpg);">
                <img src="/image2.jpg" alt="Skirt">
              </figure>
              <div id="pro-deets">
                <h3>Skirt</h3>
                <span id="price" class="holderpage">
                  £<span class="price t-price">10</span>
                </span>
              </div>
            </a>
          </li>
    
          <li class="grid-item XS Male Beige Bags Mint Sale" data-category="Sale">
            <a href="link" class="animsition-link" data-animsition-out-class="fade-out-left-lg">
              <figure style="background-image: URL(image.jpg);">
                <img src="/image2.jpg" alt="Jacket">
              </figure>
              <div id="pro-deets">
                <h3>Jacket</h3>
                <span id="price" class="holderpage">
                  £<span class="price sale">30</span>
                  <span class="price">£<span class="t-price">20</span></span>
                </span>
              </div>
            </a>
          </li>
    
      </ul>
    </div>
    </body>
    </html>

    Second Example

    I am limited to 30000 characters. Thus, I removed the HTML part from example 2. Just replace the JS of example 1 to the one from example 2 to run the example 2.

    For the second example, its routines are almost similar to those of the first example. For the second example, whenever the user select any field that associate to the filtering operation of the grid, the selected values is applied to the grid. After that, those value is then apply to location.hash. To prevent filterWithHash() to run and interpret that just created hash. The setHash() function set a variable that called gridAlreadyUpdated to true to tell the filterWithHash() there isn't a need to update anything.

    The setHash() function will only interpret hash parameters that is associated with the filtering operation. Others hashes is disregarded.

    There are also other comments that I wrote into the code that probably can help you.

    "use strict";
    $(document).ready(function(){
    // Quick Search Regex
    var qsRegex;
    // Filter Value
    var filterValue;
    // sortValue & whether to sortAscending
    var sortValue;
    var sortAscending;
    // Grid not initialize yet.
    var $grid; 
    // Last state of all the filters
    var lastState = {};
    
    /*
     * Parameter name for quicksearch, filter, and sort
     * Have this here so everything can easily be changed in one place.
     *
     */
    var quicksearchParamName = "search";
    var filterParamName = "filter";
    var sortValueParamName = "sort";
    var sortTypeParamName = "sorttype";
    
    
    /*
     * Regexes for grabbing values from hash parameter.
     *
     */
    var quicksearchRegex = RegExp(quicksearchParamName + '=([^&]+)', 'i');
    var filterRegex = RegExp(filterParamName + '=([^&]+)' , 'i');
    var sortValueRegex = RegExp(sortValueParamName + '=([^&]+)' , 'i');
    var sortTypeRegex = RegExp(sortTypeParamName + '=([^&]+)' , 'i');
    
    /* 
     * This variable is for the setHash() function to communicate with
     * the filterWithHash() function. 
     *
     * There isn't a need to build a hash string, update everything, and then
     * reinterprete that same hash string right after.
     * 
     * Thus, there isn't a need to run setHash() and then let filterWithHash()
     * run on hash update.
     */
    var gridAlreadyUpdated = false;
    
    // use value of search field to filter
    var $quicksearch = $("#quicksearch").keyup(
      debounce(function() {
        setHash(1);
      })
    );
    // debounce so filtering doesn't happen every millisecond
    function debounce(fn, threshold) {
      var timeout;
      return function debounced() {
        if (timeout) {
          clearTimeout(timeout);
        }
        function delayed() {
          fn();
          timeout = null;
        }
        setTimeout(delayed, threshold || 100);
      };
    }
    
    /*
     * Since there is only one filter group and that is sizes. 
     * It isn't necessary to be done like this. 
     * Just grab the selected value everytime the sizes select is changed.
     * However, this was still left like this.
     *
     */
    $('.filter-select').on( 'change', function( event ) {
      // Hold the filter values here.
      var filters = {};
      var $select = $( event.target );
      // Get group key
      var filterGroup = $select.attr('value-group');
      // Set filter for group
      filters[ filterGroup ] = event.target.value;
      // Assign the filter value to the global filterValue
      filterValue = concatValues( filters );
      // Execute $grid.isotope() to update with current global filter value.
      setHash(2);
    });
    
    // flatten object by concatting values
    function concatValues( obj ) {
      var value = '';
      for ( var prop in obj ) {
        value += obj[ prop ];
      }
      return value;
    }
    
    /*
     * change is-checked class on buttons
     * Only need one price-sort not two
     *
     */
    $('#price-sort').on( 'change', function() {
        setHash(3, this);
    });
    
    function getHashFilter() {
      // get filter=filterName
      var matches = location.hash.match( filterRegex );
      var hashFilter = matches && matches[1];
      return hashFilter && decodeURIComponent( hashFilter );
    }
    
    function getSearchFilter() {
      // get search=filterName
      var matches = location.hash.match( quicksearchRegex );
      var searchFilter = matches && matches[1];
      return searchFilter && decodeURIComponent( searchFilter );
    }
    
    /*
     * Get the sort param. This function will always return an array with
     * 2 indexes. If both sortValue and sortType is found then it will return
     * the values for both. Value is index 1, and type is index 2. 
     * 
     * For everything else, this function will return [null, null].
     */ 
    function getSortParam() {
      var valueMatches = location.hash.match( sortValueRegex );
      var typeMatches = location.hash.match( sortTypeRegex );
      var v = valueMatches && valueMatches[1];
      var t = typeMatches && typeMatches[1];
      
      if ( v && t ) return [decodeURIComponent( v ), decodeURIComponent( t )];
      return [ null, null ];
    }
    
    /*
     * This function will set the hash when one of the filtering field is 
     * changed. 
     *
     * Parameter whocall is utilize to know who is the caller. There can only
     * be one caller at a time. Whocall is utilize as int because comparing 
     * int is much faster than comparing string.
     *
     * whocall(1) = quicksearch
     * whocall(2) = filter
     * whocall(3) = sorting
     * 
     * In a secure environment any other whocall besides the 3 above should 
     * generate an error.
     */
    function setHash( whocall, obj ){
      var hashes = {};
      var hashStr = "";
      /* 
       * Regex can also be utilized here to change the hash string, but for
       * example, I thought this method look more clear.
       * 
       * For performance, I haven't tested regEx vs this, so I don't know.
       * Other method are available like URLSearchParams etc... but those method
       * might not be supported by all browser.
       *
       */
      if ( location.hash ){
          /* 
           * forEach can be uitlized here, but this provide better cross platform
           * compatibitliy.
           *
           */
        let temp = location.hash.substr(1).split("&");
        for ( let i = 0; i < temp.length; i++ ){
          let param = temp[i].split("=");
          // if param[0] is 0 length that is an invalid look something like &=abc.
          if ( param[0].length === 0 ) continue;
          /*
           * if more than > 2 that is also invalid but just grab the first one anyway.
           * if exactly 1 that is something like &filter=&somethingelse. So that is an 
           * empty param.
           *
           */
            let value = param.length > 1? param[1] : '';
            // This does not check if a url receive the same parameter multiple times.
            hashes[param[0]] = value;
          }
        }
        
      /*
       * If there is a quicksearch value assign that to the hashes object.
       * If not delete quicksearch name from the hashes object if there is.
       * With this way, if there was a value for quicksearch in the hash 
       * object, it will just get overwritten. If not that index will be create.
       * The delete statement is just for cosmetic. This we turn the url back
       * to without hashes if there isn't a value. 
       * However, for faster code, this can simply be done as 
       *
       *   hashes[quicksearchParamName] = $("#quicksearch").val()
       *
       * If do like the above, whether if there is a value or not, the hash
       * parameter for quicksearch will always be built.
       *
       */
      if ( whocall === 1 ){
        // 1 : quicksearch
        if ( $("#quicksearch").val() ) hashes[quicksearchParamName] = encodeURIComponent($("#quicksearch").val());
        else delete hashes[quicksearchParamName];
        qsRegex = new RegExp($("#quicksearch").val(), "gi");
        /*
         * For lastState, if setup correctly, val will give an empty string
         * or something here.
         *
         */  
        lastState["searchFilter"] = $("#quicksearch").val();
      } else if ( whocall === 2 ){
        // 2 : filter
        /*
         * If done correctly there will always be a filter value when the user
         * choose an option
         *
         */
        hashes[filterParamName] = encodeURIComponent(filterValue);
        lastState["filterValue"] = filterValue;
      } else {
        // 3 : price sort
        /*
         * If from user selecting, without an option for resetting. If done 
         * correctly, there will always be a sortValue and sortType.
         *
         */
        lastState["sortValue"] = sortValue = obj.value;
        lastState["sortType"] = obj.selectedOptions[0].getAttribute('data-sorttype');
        hashes[sortValueParamName] = encodeURIComponent(obj.value);
        hashes[sortTypeParamName] = obj.selectedOptions[0].getAttribute('data-sorttype');;
        sortAscending = hashes[sortTypeParamName] === "asc"? true : false;
      }
    
      // Update the grid.
      $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});  
      /*
       * Update the hash without making filterWithHash() update the grid.
       * Join everything in hashes together into a string. Always append &
       * after a key. But when assign to "location.hash", remove the last
       * character(extra &) from the string.
       *
       */
      for ( const k in hashes ) hashStr += k + "=" + hashes[k] + "&";
      gridAlreadyUpdated = true;
      location.hash = hashStr.substr(0, hashStr.length - 1);
    }
    
    /*
     * This function below can be customize to utilize not just only hashes
     * but also "Get Requests"
     *
     */
    function filterWithHash() {
      // If the grid is already updated, there is nothing to do.
      if ( gridAlreadyUpdated ) {
          gridAlreadyUpdated = false;
          return;
      }
      var hashFilter = getHashFilter();
      var searchFilter = getSearchFilter();
      var sortParam = getSortParam();
      /*
       * If the last time we access the value for the filters and it
       * is the same at this time. There isn't a point to re-execute the code
       */
      if ( $grid && lastState["searchFilter"] === searchFilter 
                 && lastState["filterValue"] === hashFilter
                 && lastState["sortValue"] === sortParam[0] 
                 && lastState["sortType"] === sortParam[1] ) {
          return;
      }
    
      lastState["sortValue"] = sortParam[0];
      lastState["sortType"] = sortParam[1];
      lastState["searchFilter"] = searchFilter;
      lastState["filterValue"] = hashFilter;
    
      /*
       * If searhFilter is there, utilize it.
       * Else, qsRegex is reset. That is because the user could input a value into the
       * search field and then later delete that value then press enter. If that happen 
       * and we don't reset the field, the result will not be reset.
       *
       * The same goes for hashFilter below, it is just easier to use this as an example.
       */  
      if ( searchFilter ) { 
          $('#quicksearch').val(searchFilter);
          qsRegex = new RegExp(searchFilter, "gi");
      } else {
          // searchhash could be null and that is not fine with RegExp. 
          // Hence, we give it an empty string.
          $('#quicksearch').val("");
          qsRegex = new RegExp("", "gi");
      }
    
      /* 
       * Refer to comment of searchFilter right above 
       *
       * Also, this is for one filter group. If you need to work out to multiple 
       * filter group, you would have to split them by the . go through each
       * index, and see if they are valid values. 
       *
       * If some are valid and some are not, disregard the invalid and use the valid.
       * If none are valid, disregard all.
       * 
       */
      if ( hashFilter ) {
        var selectValue = $('select[id="sizes"]').find('option[value="'+ hashFilter +'"]');
        // Only search for a value if it is found within the select fields, else disregard it.
        if ( selectValue.length > 0 ){
          selectValue.prop('selected', 'selected');
          filterValue = hashFilter;
        }
      } else {
        // filterValue will become null or empty whichever. But that is fine.
        filterValue = hashFilter;
        $('select[id="sizes"]').prop('selectedIndex',0);
      }
    
      /*
       * getSortParam will always return two index. It just whether if they both have
       * values or both null.
       *
       * If sortParam is [null, null] or its values are invalid. Turn sortValue and
       * sortAscending to null.
       *
       * If sortParam is [null, null] prop the default select for select group 
       * with the id price-sort.
       */
      if ( sortParam[0] ){
        // search price sort select group to see if the hash is valid.
        var sortObj = $('#price-sort').find('option[value="'+ sortParam[0] +'"][data-sorttype="'+ sortParam[1] +'"]');
        // If hash is valid prob the field
        // Else reset the field
        if ( sortObj.length > 0 ){
          sortObj.prop('selected', true);
          sortValue = sortParam[0];
          sortAscending = sortParam[1] === "asc"? true : false;
        } else {
          sortValue = null;
          sortAscending = null;
          $('select[id="price-sort"]').prop('selectedIndex', 0);
        }
      } else {
        sortValue = null;
        sortAscending = null;
        $('select[id="price-sort"]').prop('selectedIndex', 0);
      }
    
      /*
       * If $grid is not initialize, it will get initialize. 
       * This will only happen on first run.
       * One grid is initilized, everytime grid.isotope() is run
       * without any value, grid will be updated to what initilized below.
       * Thus, each later run of isotope(), the filter will look at both,
       * the searchResult and the qsRegex if they are available.
       *
       */
      if ( !$grid ) {
        $grid = $(".grid").isotope({
                  itemSelector: ".grid-item",
                  layoutMode: "fitRows",
                  getSortData: {
                    price: '.t-price parseInt',
                    category: '[data-category]',
                  },
                  sortBy: sortValue ,
                  sortAscending: sortAscending,
                  filter: function() {
                    var $this = $(this);
                    var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
                    var selectResult = filterValue ? $this.is(filterValue) : true;
                    return searchResult && selectResult;
                  }              
                });
      /*
       * When grid.isotope() is execute with sortValue, if that sortValue === null 
       * then grid.isotope() will not execute the sort parameter. That is for the 
       * isotope version of when this was first written. The code may need to 
       * be updated for future version if the behaviour of the isotope() function
       * change.
       *
       */
      } else $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
    }
    
    /* 
     * Trigger filter with hash to initialize grid
     * and also to check the url for hash.
     */
    filterWithHash();
    
    // Bind the filterWithHash function to the hashchange event.
    $(window).on( 'hashchange', filterWithHash );
    
    });