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>
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 );
});
Answer : Part 2
For this answer, it is an example 3. I most likely will not be able to provide comments on this code as this code reached the character limitation. A few commenting in the codes had to be taken out. Prefer to example 1 and example 2 for those missing comments.
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://unpkg.com/imagesloaded#4/imagesloaded.pkgd.min.js"></script>
<script src="https://unpkg.com/isotope-layout#3/dist/isotope.pkgd.js"></script>
<script>
"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;
/*
* Assiging filterElements here for easy modification.
*
* Currently the hash is something like this "filter=.XS.Jackets.Black"
* It can always be changed to "filter=size:.XS+type:Jackets+color:black"
* with some modification to the code.
*/
var $filterElements = $('.filter-select');
/*
* The preceding $ above mean that $filterElements is a jquery object.
* The below filterElements is the array of all the elements
* of $filterElements converted to jquery object.
*
* This was done like this so each time a select group is needed it doesn't
* have to be converted to an object of jquery.
*/
var filterElements = [];
for ( let i = 0; i < $filterElements.length; i++ ){
filterElements[i] = ($($filterElements[i]));
}
/*
* Reset filter button, remove if not need.
*
*/
$("#btn-reset-filter").on('click', function( event ){
$filterElements.prop('selectedIndex',0);
// only trigger the event for one element.
filterElements[0].trigger("change");
});
// 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);
};
}
/*
* When any of the select field with class filter is change
* we cycle through all the fields and grab all the values for each field
* and concat them into the filterValue.
*
* Over here filterElements that is being used is the JQuery object.
*/
$filterElements.on( 'change', function( event ) {
filterValue = "";
for ( let i = 0; i < $filterElements.length; i++ ){
if ( $filterElements[i].selectedIndex > 0 ) {
filterValue += $filterElements[i].value;
}
}
// Use set hash to update the $grid and set the hash.
setHash(2);
});
/*
* change is-checked class on buttons
* Only need one price-sort not two
*
*/
$('#price-sort').on( 'change', function() {
setHash(3, this);
});
function removeDisabled() {
$filterElements.prop('disabled', false);
$('#quicksearch').prop('disabled', false);
$('#price-sort').prop('disabled', false);
}
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 = "";
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;
}
/*
* If the fields are not disabled then update the grid if grid already intialized.
* Otherwise, the initializer will do that. For that use the code in this comment
* instead.
*
* if ( $grid ) $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
*/
$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;
/*
* Note that the lastState["filterValue"] for here is
* the string value of hash filter and not the actual
* processed filterValue.
*
*/
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
*
* This will split the hash string by the "." and then check for their values.
* If they are valid, it will set the filter value by the valid one. It will
* disregard all the invalid.
*
* This search is based on the filterElements. For each filterElements,
* the code will search for a match in the split array. If that is found,
* that value is added to the filterValue string.
*
* Therefore, any duplicate will not be taken into consideration.
* Duplicates only can become a problem if values between each filterElements
* are not unique in nature.
*/
if ( hashFilter ) {
hashFilter = hashFilter.split(/(?=\.)/);
filterValue = "";
for ( let i = 0; i < filterElements.length; i++ ){
let foundedValue = false;
/*
* This will search through the entire splitted hash array. But can
* be done to search at maximum the same amount of filterElements
* that are available.
*
* Note that, this does not check or prop the All select field
* as that field now does not contain any values.
*
* Also how many index turned to null can be keep tracked of. That
* is because if there are 2 indexes and both turned to null but there
* are 20 filter elements then There is no point executing the code
* for the other 18 elements.
*
* However, for a smaller amount of filter elements, keeping track
* of how many elements turned to null may create an overhead cost.
*/
for ( let j = 0; j < hashFilter.length; j++ ){
if ( hashFilter[j] === null ) continue;
let selectValue = filterElements[i].find('option[value="'+ hashFilter[j] +'"]');
/*
* If selectValue is found then select the value in the element
* and add that value to the filterValue string.
*
* One a value is founded and used, that value will be turned
* to null and will not be reused.
*
* Make sure the value of each select group is unique.
*/
if ( selectValue.length > 0 ){
selectValue.prop('selected', 'selected');
filterValue += hashFilter[j];
hashFilter[j] = null;
foundedValue = true;
break;
}
}
/*
* If the hash string did not contain a value for this element,
* prop selected index 0(reset the select field).
*
*/
if ( foundedValue === false ) filterElements[i].prop('selectedIndex',0);
}
/*
* If after cycle through the hash above and filterValue's length is still 0
* then a default value will be prop for everything.
*
* Notice the changes here, where $filterElements that is being used
* is preceding with the $.
*/
if ( filterValue.length === 0 ) $filterElements.prop('selectedIndex',0);
} else {
// filterValue will become null or empty whichever. But that is fine.
filterValue = hashFilter;
$filterElements.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 ) {
$grid = $(".grid").isotope({
itemSelector: ".grid-item",
layoutMode: "fitRows",
getSortData: {
price: '.t-price parseInt',
category: '[data-category]',
dateAdded: function( itemElem ){
// Sort by date: return the timestamp of dateAdded.
return new Date($(itemElem).find('.dateAdded').text()).getTime();
},
/*
* Sort by item with class New.
* If the item contain class New, it will be assigned with a value of 0.
* Else, it will be assigned with a value of 1.
*
* There shouldn't be a sort descending for New. A field like that
* wasn't configured. Therefore, hash wouldn't be able to sort
* New in a descending way.
*/
New: function( itemElem ){
return $(itemElem).hasClass('New')? 0 : 1;
}
},
sortBy: sortValue ,
sortAscending: sortAscending,
filter: function() {
/* Note that current filtering using class name. */
var $this = $(this);
var searchResult = qsRegex ? $this.text().match(qsRegex) : true;
var selectResult = filterValue ? $this.is(filterValue) : true;
return searchResult && selectResult;
}
});
} else $grid.isotope({ sortBy: sortValue , sortAscending: sortAscending});
}
$('.grid').imagesLoaded( function() {
filterWithHash();
$(window).on( 'hashchange', filterWithHash );
removeDisabled();
});
});
</script>
</head>
<body>
<div id="sort-filter">
<!-- Short Div -->
<div id="sort">
<select id="price-sort" class="select-css form-control long" disabled>
<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>
<option data-sorttype="des" value="dateAdded">Newest</option>
<option data-sorttype="asc" value="dateAdded">Oldest</option>
<option data-sorttype="asc" value="New">New</option>
<option data-sorttype="asc" value="original-order">Original Order</option>
</select>
</div>
<!-- Filter Div -->
<div class="filters">
<select class="filter-select select-css short" value-group="sizes" id="sizes" disabled>
<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>
<select class="filter-select select-css long" value-group="gender" id="gender" disabled>
<option selected disabled> Gender </option>
<option value="">All</option>
<option value=".Male">Male</option>
<option value=".Female">Female</option>
<option value=".Genderless">Genderless</option>
</select>
<select class="filter-select select-css short" value-group="colour" id="colour" disabled>
<option selected disabled> Colour </option>
<option value="">All</option>
<option value=".Black">Black</option>
<option value=".Blue">Blue</option>
<option value=".Brown">Brown</option>
<option value=".Cream">Cream</option>
<option value=".Other">Other</option>
</select>
<select class="filter-select select-css long" value-group="type" id="type" disabled>
<option selected disabled> Type </option>
<option value="">All</option>
<option value=".Bags">Bags</option>
<option value=".Bottoms">Bottoms</option>
<option value=".Co-ords">Co-ords</option>
<option value=".Jackets">Jackets</option>
</select>
<select class="filter-select select-css short" value-group="brand" id="brand" disabled>
<option selected disabled> Brand </option>
<option value="">All</option>
<option value=".Adidas">Adidas</option>
<option value=".American Sports Teams">American Sports Teams</option>
</select>
<button type="button" id="btn-reset-filter">Reset Filter</button>
<input type="text" id="quicksearch" class="quicksearch" placeholder="Search" / disabled>
</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="https://live.staticflickr.com/65535/48297243402_ea74392016_w_d.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>
<div class="dateAdded">1986-10-21</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="https://live.staticflickr.com/748/20445410340_c1a0fe6a6a_w_d.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>
<div class="dateAdded">2008-9-30</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="https://live.staticflickr.com/724/20624833532_4b08d803b7_w_d.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>
<div class="dateAdded">1986-10-22</div>
</a>
</li>
<li class="grid-item XS Male Beige Bags Mint Sale Jackets" 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="https://live.staticflickr.com/590/20447259789_9114edf13a_w_d.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>
<div class="dateAdded">2018-7-30</div>
</a>
</li>
<li class="grid-item XXL 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="https://live.staticflickr.com/4148/4973470198_b3af07ca8c_w_d.jpg" alt="Jacket">
</figure>
<div id="pro-deets">
<h3>Wallet</h3>
<span id="price" class="holderpage">
£<span class="price sale">30</span>
<span class="price">£<span class="t-price">50</span></span>
</span>
</div>
<div class="dateAdded">2017-5-20</div>
</a>
</li>
</ul>
</div>
</body>
</html>
Photo from Flickr with links:
Photo 1 - (400x267)
Photo 2 - (400x225)
Photo 3
- (400x225)
Photo 4 - (400x225)
Photo 5 - (400x250)
Im working on Magento 2.1.1 . When I open product page from product list page i cant able to see category name on breadcrumbs . It just shows homepage url followed by product name . Very rarely I can able to see full breadcrumbs with category name .
breadcrumbs - category name is missing .
Could someone please give me some ideas to solve this issue?
This issue is already reported to Magento 2 GitHub repository here:
https://github.com/magento/magento2/issues/7967
There is a gist linked in the comments which has a workaround:
https://gist.github.com/mbahar/82703c4b95d924d9bc8e6990202fdeba
I've found solution, approved by Magento team. Because if you use approaches with block rewrites or plugins and you have some extensions that use Breadcrumbs block or Magento\Catalog\Controller\Product\View.php, you'll have problems.
So, you have to customize just app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js.
So, here is a full code:
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
define([
'jquery',
'Magento_Theme/js/model/breadcrumb-list'
], function ($, breadcrumbList) {
'use strict';
return function (widget) {
$.widget('mage.breadcrumbs', widget, {
options: {
categoryUrlSuffix: '',
useCategoryPathInUrl: false,
product: '',
menuContainer: '[data-action="navigation"] > ul'
},
/** #inheritdoc */
_render: function () {
this._appendCatalogCrumbs();
this._super();
},
/**
* Append category and product crumbs.
*
* #private
*/
_appendCatalogCrumbs: function () {
var categoryCrumbs = this._resolveCategoryCrumbs();
categoryCrumbs.forEach(function (crumbInfo) {
breadcrumbList.push(crumbInfo);
});
if (this.options.product) {
breadcrumbList.push(this._getProductCrumb());
}
},
/**
* Resolve categories crumbs.
*
* #return Array
* #private
*/
_resolveCategoryCrumbs: function () {
var menuItem = this._resolveCategoryMenuItem(),
categoryCrumbs = [];
if (menuItem !== null && menuItem.length) {
categoryCrumbs.unshift(this._getCategoryCrumb(menuItem));
while ((menuItem = this._getParentMenuItem(menuItem)) !== null) {
categoryCrumbs.unshift(this._getCategoryCrumb(menuItem));
}
}
return categoryCrumbs;
},
/**
* Returns crumb data.
*
* #param {Object} menuItem
* #return {Object}
* #private
*/
_getCategoryCrumb: function (menuItem) {
return {
'name': 'category',
'label': menuItem.text(),
'link': menuItem.attr('href'),
'title': ''
};
},
/**
* Returns product crumb.
*
* #return {Object}
* #private
*/
_getProductCrumb: function () {
return {
'name': 'product',
'label': this.options.product,
'link': '',
'title': ''
};
},
/**
* Find parent menu item for current.
*
* #param {Object} menuItem
* #return {Object|null}
* #private
*/
_getParentMenuItem: function (menuItem) {
var classes,
classNav,
parentClass,
parentMenuItem = null;
if (!menuItem) {
return null;
}
classes = menuItem.parent().attr('class');
classNav = classes.match(/(nav\-)[0-9]+(\-[0-9]+)+/gi);
if (classNav) {
classNav = classNav[0];
parentClass = classNav.substr(0, classNav.lastIndexOf('-'));
if (parentClass.lastIndexOf('-') !== -1) {
parentMenuItem = $(this.options.menuContainer).find('.' + parentClass + ' > a');
parentMenuItem = parentMenuItem.length ? parentMenuItem : null;
}
}
return parentMenuItem;
},
/**
* Returns category menu item.
*
* Tries to resolve category from url or from referrer as fallback and
* find menu item from navigation menu by category url.
*
* #return {Object|null}
* #private
*/
_resolveCategoryMenuItem: function () {
var categoryUrl = this._resolveCategoryUrl(),
menu = $(this.options.menuContainer),
categoryMenuItem = null;
if (categoryUrl && menu.length) {
categoryMenuItem = menu.find('a[href="' + categoryUrl + '"]');
}
return categoryMenuItem;
},
/**
* Returns category url.
*
* #return {String}
* #private
*/
_resolveCategoryUrl: function () {
var categoryUrl;
if (this.options.useCategoryPathInUrl) {
// In case category path is used in product url - resolve category url from current url.
categoryUrl = window.location.href.split('?')[0];
categoryUrl = categoryUrl.substring(0, categoryUrl.lastIndexOf('/')) +
this.options.categoryUrlSuffix;
} else {
// In other case - try to resolve it from referrer (without parameters).
categoryUrl = document.referrer;
if (categoryUrl.indexOf('?') > 0) {
categoryUrl = categoryUrl.substr(0, categoryUrl.indexOf('?'));
}
}
return categoryUrl;
}
});
return $.mage.breadcrumbs;
};
});
And here is a link to merge request: ENGCOM-1707
I've found a solution for the above issue. Kindly check the below js file.
vendor/magento/module-catalog/view/frontend/web/js/product/breadcrumbs.js
The breadcrumbs.js file has taken some classes by using the menu. If you are customizing the menu, the category name is missed in the breadcrumbs-Product detail page.
Menu must have the following class:
1)Menu nav section must have "data-action="navigation".
2)Menu li element must have "category-item" class.
3)The parent li element must have "nav-1","nav-2",.. class and the sub child li("nav-1")element must have "nav-1-1","nav-1-2" and so on..
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
define([
'jquery',
'Magento_Theme/js/model/breadcrumb-list'
], function ($, breadcrumbList) {
'use strict';
return function (widget) {
// calling menu class and data-action "categoryItemSelector","menuContainer"**
$.widget('mage.breadcrumbs', widget, {
options: {
categoryUrlSuffix: '',
useCategoryPathInUrl: false,
product: '',
categoryItemSelector: '.category-item',
menuContainer: '[data-action="navigation"] > ul'**
},
/** #inheritdoc */
_render: function () {
this._appendCatalogCrumbs();
this._super();
},
/**
* Append category and product crumbs.
*
* #private
*/
_appendCatalogCrumbs: function () {
var categoryCrumbs = this._resolveCategoryCrumbs();
categoryCrumbs.forEach(function (crumbInfo) {
breadcrumbList.push(crumbInfo);
});
if (this.options.product) {
breadcrumbList.push(this._getProductCrumb());
}
},
/**
* Resolve categories crumbs.
*
* #return Array
* #private
*/
_resolveCategoryCrumbs: function () {
var menuItem = this._resolveCategoryMenuItem(),
categoryCrumbs = [];
if (menuItem !== null && menuItem.length) {
categoryCrumbs.unshift(this._getCategoryCrumb(menuItem));
while ((menuItem = this._getParentMenuItem(menuItem)) !== null) {
categoryCrumbs.unshift(this._getCategoryCrumb(menuItem));
}
}
return categoryCrumbs;
},
/**
* Returns crumb data.
*
* #param {Object} menuItem
* #return {Object}
* #private
*/
_getCategoryCrumb: function (menuItem) {
return {
'name': 'category',
'label': menuItem.text(),
'link': menuItem.attr('href'),
'title': ''
};
},
/**
* Returns product crumb.
*
* #return {Object}
* #private
*/
_getProductCrumb: function () {
return {
'name': 'product',
'label': this.options.product,
'link': '',
'title': ''
};
},
/**
* Find parent menu item for current.
*
* #param {Object} menuItem
* #return {Object|null}
* #private
*/
_getParentMenuItem: function (menuItem) {
var classes,
classNav,
parentClass,
parentMenuItem = null;
if (!menuItem) {
return null;
}
classes = menuItem.parent().attr('class');
// calling menu nav class**********
**classNav = classes.match(/(nav\-)[0-9]+(\-[0-9]+)+/gi);**
if (classNav) {
classNav = classNav[0];
parentClass = classNav.substr(0, classNav.lastIndexOf('-'));
if (parentClass.lastIndexOf('-') !== -1) {
parentMenuItem = $(this.options.menuContainer).find('.' + parentClass + ' > a');
parentMenuItem = parentMenuItem.length ? parentMenuItem : null;
}
}
return parentMenuItem;
},
/**
* Returns category menu item.
*
* Tries to resolve category from url or from referrer as fallback and
* find menu item from navigation menu by category url.
*
* #return {Object|null}
* #private
*/
_resolveCategoryMenuItem: function () {
var categoryUrl = this._resolveCategoryUrl(),
menu = $(this.options.menuContainer),
categoryMenuItem = null;
if (categoryUrl && menu.length) {
categoryMenuItem = menu.find(
this.options.categoryItemSelector +
' > a[href="' + categoryUrl + '"]'
);
}
return categoryMenuItem;
},
/**
* Returns category url.
*
* #return {String}
* #private
*/
_resolveCategoryUrl: function () {
var categoryUrl;
if (this.options.useCategoryPathInUrl) {
// In case category path is used in product url - resolve category url from current url.
categoryUrl = window.location.href.split('?')[0];
categoryUrl = categoryUrl.substring(0, categoryUrl.lastIndexOf('/')) +
this.options.categoryUrlSuffix;
} else {
// In other case - try to resolve it from referrer (without parameters).
categoryUrl = document.referrer;
if (categoryUrl.indexOf('?') > 0) {
categoryUrl = categoryUrl.substr(0, categoryUrl.indexOf('?'));
}
}
return categoryUrl;
}
});
return $.mage.breadcrumbs;
};
});
I haven't tried the above workaround, but people say it's not always working.
Meanwhile the following seems to be working nicely (I've just tested it):
https://github.com/magento/magento2/issues/7967#issuecomment-315310890
[Vendor]/[Module]/etc/frontend/events.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="catalog_controller_product_init_after">
<observer instance="[Vendor]\[Module]\Observer\Catalog\Product\FullPathBreadcrumbs" name="addProductFullPathBreadcrumbs"/>
</event>
</config>
[Vendor]/[Module]//Observer/Catalog/Product/FullPathBreadcrumbs
<?php
namespace [Vendor]\[Module]\Observer\Catalog\Product;
class FullPathBreadcrumbs implements \Magento\Framework\Event\ObserverInterface
{
protected $_registry;
protected $_categoryRepository;
public function __construct(
\Magento\Framework\Registry $registry,
\Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository
) {
$this->_registry=$registry;
$this->_categoryRepository = $categoryRepository;
}
/**
* Execute observer
*
* #param \Magento\Framework\Event\Observer $observer
* #return void
*/
public function execute(
\Magento\Framework\Event\Observer $observer
) {
$product = $observer->getEvent()->getProduct();
if ($product != null && !$this->_registry->registry('current_category')) {
$cats = $product->getAvailableInCategories();
if(sizeof($cats)===1){
$last = $cats[0];
}else{
end($cats);
$last = prev($cats);
}
if($last) {
$category = $this->_categoryRepository->get($last);
$this->_registry->register('current_category', $category, true);
}
}
}
}
My page gets a response from response_ajax.php with this code:
<input class="btn" name="send_button" type="button" value="check"
onClick=
"xmlhttpPost('/response_ajax.php',
'MyForm',
'MyResult',
'<img src=/busy.gif>')";
return false;"
>
I get a response; however, jQuery scripts don't work with an arrived code. I'm trying to add script inside response_ajax.php, but nothing happens:
<?php
// ... //
echo '
<div id="whois-response">
<pre>' .$str. '</pre>
</div>
<script>
(function($){
$(function(){
alert("loaded");
});
})(jQuery);
</script>
';
?>
xmlhttpPost function:
function xmlhttpPost(strURL,formname,responsediv,responsemsg) {
var xmlHttpReq = false;
var self = this;
// Xhr per Mozilla/Safari/Ie7
if (window.XMLHttpRequest) {
self.xmlHttpReq = new XMLHttpRequest();
}
// per tutte le altre versioni di IE
else if (window.ActiveXObject) {
self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
self.xmlHttpReq.open('POST', strURL, true);
self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
self.xmlHttpReq.onreadystatechange = function() {
if (self.xmlHttpReq.readyState == 4) {
// Quando pronta, visualizzo la risposta del form
updatepage(self.xmlHttpReq.responseText,responsediv);
}
else{
// In attesa della risposta del form visualizzo il msg di attesa
updatepage(responsemsg,responsediv);
}
}
self.xmlHttpReq.send(getquerystring(formname));
}
function getquerystring(formname) {
var form = document.forms[formname];
var qstr = "";
function GetElemValue(name, value) {
qstr += (qstr.length > 0 ? "&" : "")
+ escape(name).replace(/\+/g, "%2B") + "="
+ escape(value ? value : "").replace(/\+/g, "%2B");
//+ escape(value ? value : "").replace(/\n/g, "%0D");
}
var elemArray = form.elements;
for (var i = 0; i < elemArray.length; i++) {
var element = elemArray[i];
var elemType = element.type.toUpperCase();
var elemName = element.name;
if (elemName) {
if (elemType == "TEXT"
|| elemType == "TEXTAREA"
|| elemType == "PASSWORD"
|| elemType == "BUTTON"
|| elemType == "RESET"
|| elemType == "SUBMIT"
|| elemType == "FILE"
|| elemType == "IMAGE"
|| elemType == "HIDDEN")
GetElemValue(elemName, element.value);
else if (elemType == "CHECKBOX" && element.checked)
GetElemValue(elemName,
element.value ? element.value : "On");
else if (elemType == "RADIO" && element.checked)
GetElemValue(elemName, element.value);
else if (elemType.indexOf("SELECT") != -1)
for (var j = 0; j < element.options.length; j++) {
var option = element.options[j];
if (option.selected)
GetElemValue(elemName,
option.value ? option.value : option.text);
}
}
}
return qstr;
}
function updatepage(str,responsediv){
document.getElementById(responsediv).innerHTML = str;
}
I may be wrong, but I'm pretty sure you can't do multiline strings unless it is configured to do so (and running a newer version of PHP):
echo '
<div id="whois-response">
<pre>' .$str. '</pre>
</div>
<script>
(function($){
$(function(){
alert("loaded");
});
})(jQuery);
</script>
';
Try changing that to:
echo <<<EOD
<div id="whois-response">
<pre> $str </pre>
</div>
<script>
(function($){
$(function(){
alert("loaded");
});
})(jQuery);
</script>
EOD;
I think your AJAX response is a PHP error instead of the script you think it is returning.
Got it to work by adding jQuery staff as a function
function updatepage(str,responsediv){
document.getElementById(responsediv).innerHTML = str;
(function($){
$(function(){
$('html').my_jQuery_staff();
});
})(jQuery);
}
Main JavaScript file with jQuery:
// ~~ jQuery ~~
$(document).ready(function () {
$.fn.my_jQuery_staff= function() {
return this.each(function() {
// Include jQuery staff here.
});
};
$('html').my_jQuery_staff();
});
I am using a Mediawiki based website. The site is http://www.DragonFallRPG.com The widget in question is the 'Orion's Dice Box' in the left column of the site.
Not sure if that has any bearing on this but here goes. I have a custom div called 'dice' with a content destination div called 'result'. Below the 'result' div is a form for selecting a number of dice, and the number of sides for those dice. There is a processing script, which is tested working to provide a randomized result as if those dice were thrown. The problem is in the calling of one or more functions, I think. I found the AJAX method for getting the user input via 'get' somewhere on the web and no longer have any idea where it came from. I will include the files below.
dice_header.php (include file for <head> portion of webpage)
<style>
<!--[if IE] -- long buttons / button width in IE fix>
<style>.button{width:1;}</style>
<![endif]-->
</style>
<?php $javafile = dirname(__FILE__).'/ajax_engine.js'; ?>
<script type="text/javascript" src= "<?php echo $javafile ?>" ></script>
<script type="text/javascript">
function submit_dice() {
// Get form values
var no_of_dice = document.getElementById('dice').value;
var no_of_sides = document.getElementById('sides').value;
// Construct URL
<?php $handlerfile = dirname(__FILE__).'/handler.php' ?>
url = '<?php echo $handlerfile; ?>' + '?no_of_dice=' + escape(no_of_dice) + '&no_of_sides=' + escape(no_of_sides);
var xend = url.lastIndexOf("/") + 1;
var base_url = url.substring(0, xend);
alert('Handlerfile URL = ' + url + '\r\n\r\n Escape URL = ' + escape(url) + '\r\n\r\n # of dice = ' + no_of_dice + '\r\n # of Sides = ' + no_of_sides);
alert('url for ajax_get = ' + url);
ajax_get (url, 'result');
}
</script>
The above code is an include in the header of the index.php
The function call for ajax_get seems to be where it breaks down in the process in the above code. I don't know if it requires the http portion of the url or not. I don't know if the escape url is required or not. I'm hesitant to monkey with the script any further without guidance.
The code that follows is the div block for the widget I'm trying to create
dice.php (include file for my widget / div block)
<div id="result" style="text-align:center;
word-wrap: break-word;
width:100px;
font-weight:bold;
font-size:large;
border:1px blue solid;
margin:0;">
<?php
//$filename = dirname(__FILE__).'/ajax_engine.js';
//$handlerfile = dirname(__FILE__).'/handler.php';
if (file_exists($handlerfile)) {
echo "Handler file path OK";
echo 'alert(\'Handler file path = "' . $handlerfile . '"\');';
die();
} else {
echo "BAD handler file path!";
}
?>
</div>
<table border="0" cellspacing="0" cellpadding="0" style="margin:0; padding:0px;" >
<tr>
<td><select name="dice" id="dice" size="1" style="margin:0px;">
<?php
for ($i = 1; ; $i++) {
if ($i > 20) {
break;
}
if ($i == 1) {
echo "<option value=$i selected>$i</option>\n";
} else {
echo "<option value=$i>$i</option>\n";
}
}
?>
</select></td>
<td><select name="sides" id="sides" size="1" style="margin:0px;">
<option value="4">d4</option>
<option value="6">d6</option>
<option value="8">d8</option>
<option value="10">d10</option>
<option value="12">d12</option>
<option value="20" selected>d20</option>
<option value="100">d100</option>
</select>
</td>
</tr><tr>
<td colspan="2">
<input type="button" onclick="submit_dice();" value="Roll Dice" style="width:100px;" />
</td></tr>
</table>
<!--
Psuedo vs. True Random Numbers
http://www.phpfive.net/pseudo-random_php_functions_and_truly_random_number_generators_article2.htm
-->
Next follows the javascript engine I'm using to begin the AJAX functionality... Mediawiki has it's own built in AJAX - but I have no familiarity with it and tried finding a less complicated working version else where that I could tweak - resulting in this headache.
Several alert popup calls made to help with debugging, but I'm lost, and none of these alerts are actually being called... I can't tell why.
// JavaScript Document "javascript_engine.js"
// Get base url
url = document.location.href;
var base_url = "http://";
alert('base_url = ' + base_url);
xend = url.lastIndexOf("/") + 1;
var base_url = url.substring(0, xend);
var ajax_get_error = false;
alert('ajax_engine.js called');
function ajax_do (url) {
// Does URL begin with http?
alert('url.substring(0, 4) = ' + url);
if (url.substring(0, 4) != 'http') {
url = base_url + url;
}
// Create new JS element
var jsel = document.createElement('SCRIPT');
jsel.type = 'text/javascript';
jsel.src = url;
// Append JS element (therefore executing the 'AJAX' call)
document.body.appendChild (jsel);
return true;
}
function ajax_get (url, el) {
// Has element been passed as object or id-string?
if (typeof(el) == 'string') {
el = document.getElementById(el);
}
// Valid el?
if (el == null) { return false; }
alert(url.substring(0, 4));
// Does URL begin with http?
if (url.substring(0, 4) != 'http') {
url = base_url + url;
}
// Create getfile URL
getfile_url = base_url + 'getfile.php?url=' + escape(url) + '&el=' + escape(el.id);
// Do Ajax
ajax_do (getfile_url);
return true;
}
Following is getfile.php
<?php //getfile.php -- used for addressing visual part of code
// Get URL and div
if (!isset($_GET['url'])) { die(); } else { $url = $_GET['url']; }
if (!isset($_GET['el'])) { die(); } else { $el = $_GET['el']; }
// echo 'alert(\'URL in getfile.php = \'); $url';
// Make sure url starts with http
if (substr($url, 0, 4) != 'http') {
// Set error
echo 'alert(\'Security error; incorrect URL!\');';
die();
}
// Try and get contents
$data = #file_get_contents($url);
if ($data === false) {
// Set error
echo 'alert(\'Unable to retrieve "' . $url . '"\');';
die();
}
// Escape data
$data = str_replace("'", "\'", $data);
$data = str_replace('"', "'+String.fromCharCode(34)+'", $data);
$data = str_replace ("\r\n", '\n', $data);
$data = str_replace ("\r", '\n', $data);
$data = str_replace ("\n", '\n', $data);
?>
el = document.getElementById('<?php echo $el; ?>');
el.innerHTML = '<?php echo $data; ?>';
Following is the form processor, generating the random numbers result for the AJAX output/update.
<?php // handler.php
/////////////////////////////////////////////////////////////////
// Random Dice Value Generator v1.0 //
// http://www.dragonfallrpg.com //
// Orion Johnson Copyright 2007 //
// //
// This script is used to create a random number based //
// values from the user's input //
/////////////////////////////////////////////////////////////////
/* double rolldice(int, int)
* - generates a random value based on the numbers passed as an argument
* - maximum iterations = 20 (can be changed in the user form)
* - maximum number of sides per function call = 4, 6, 8, 10, 12, 20, or 100 (can be changed)
*
* Usage: To generate a random total value as if one had thrown that many dice:
* Note: Future revisions may include the ability to add additional lines to the user form
* to mix types of simulated dice being thrown.
*
* array $no_of_dice(x-1); array value "x" taken from user form
* var $no_of_sides; value taken from user form
* var $total_value; sum of values from entire array
* echo $total_value;
*/
// Check variables
if (empty($_GET['no_of_dice'])) {
die ('<span style="color:red;">Number of dice value invalid!</span>');
}
if (empty($_GET['no_of_sides'])) {
die ('<span style="color:red;">Number of sides value invalid!</span>');
}
// seed with microseconds
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 1000003);
}
function rolldice()
{
$total_value = 0; /* sum of values from entire array */
srand(make_seed()); /* seed random number generator // 1,000,003 is a prime number */
/* start loop structure from 0 to $no_of_dice */
for($i = 0; $i < $_GET['no_of_dice']; $i++)
{
$randnum = rand(1, $_GET['no_of_sides']);
$total_value = $total_value + $randnum;
}
/* end loop */
/* print/return results to the screen */
// echo 'Total value for dice: ' + rolldice();
return $total_value;
}
// Taken from http://www.sebflipper.com/?page=code&file=password.php
// for array iteration see also: http://www.php-scripts.com/php_diary/122799.php3
?>
If there is a simpler way to perform a div update with a random result based on form input, I'm all ears. This has been a headache for too long for me. I'm no code-head, just know enough to tinker and make some things work and understand most things when explained.
I haven't read through all the code, but since MediaWiki 1.17.0 there's a feature called ResourceLoader (if you have older version you should upgrade), which you can use for this purpose.
You can make the whole code into an extension to have it organized in a directory (let's say extensions/DiceBox). The DiceBox.php file in that folder would then be along these lines:
<?php
if( !defined( 'MEDIAWIKI' ) ) {
die();
}
$wgExtensionFunctions[] = 'DiceBoxInit';
$wgHooks['SkinTemplateToolboxEnd'][] = 'DiceBoxOnSkinTemplateToolboxEnd';
$wgResourceModules['ext.DiceBox'] = array(
'localBasePath' => dirname( __FILE__ ),
'remoteExtPath' => 'DiceBox',
'scripts' => 'ext.DiceBox.js',
'dependencies' => 'jquery'
);
function DiceBoxInit() {
global $wgOut;
$wgOut->addModules( 'ext.DiceBox' );
}
?>
<?php
function DiceBoxOnSkinTemplateToolboxEnd() {
$sides = array( 4, 6, 8, 10, 12, 20, 100 );
?>
</ul>
</div>
</div>
<div class="portlet" id="p-dicebox">
<h5>Orion's Dice Box</h5>
<div class="pBody">
<div id="dice-result" style="display: none;"></div>
<form id="dice-form">
<select name="no_of_dice">
<?php
for( $i = 1; $i <= 20; $i++ ) {
echo '<option value="', $i, '">', $i, '</option>';
}
?>
</select>
<select name="no_of_sides">
<?php
foreach( $sides as $n ) {
echo '<option value="', $n, '">d', $n, '</option>';
}
?>
</select>
<input type="button" value="Roll Dice" id="dice-roll" />
</form>
<?php
return true;
}
This code outputs the code box HTML in the sidebar and registers the following JavaScript file (ext.DiceBox.js) to be available on the page:
jQuery( document ).ready( function( $ ) {
$( '#dice-roll' ).click( function() {
$.get( mw.config.get( 'wgExtensionAssetsPath' ) + '/DiceBox/handler.php',
$( '#dice-form' ).serialize(), function( data )
{
$( '#dice-result' ).html( data ).append( '<hr />' ).show();
} );
} );
} );
This code simply uses jQuery (which is bundled with MediaWiki as of 1.16.0) to send a request to the server when the button is clicked and displays the result in the box.
In the handler.php file, there's no place where the random number gets output, so you need to add echo rolldice(); before the ?>.
Finally, to make the extensions fully work, add require_once $IP . '/extensions/DiceBox/DiceBox.php'; to the bottom of LocalSettings.php.