Isotope with Filter Count and imagesLoaded - javascript

I have a large grid of objects that I'm filtering with isotope. I'm using combination filters and a search box, and I'm also using imagesLoaded (necessary because of the large number of items). That all works ok.
I also want to print the number of items filtered. I followed instructions here
And here's my code:
jQuery(document).ready(function($){
// quick search regex
var qsRegex;
// init Isotope
var $grid = $('.grid').imagesLoaded( function() {
$grid.isotope({
itemSelector: '.status',
getSortData: {
name: '.elemsort',
},
sortBy : 'name',
});
});
var iso = $grid.data('isotope');
var $filterCount = $('.filter-count');
// store filter for each group
var filters = {};
$('.filtersbutton').on( 'click', '.button', function() {
var $this = $(this);
// get group key
var $buttonGroup = $this.parents('.button-group');
var filterGroup = $buttonGroup.attr('data-filter-group');
// set filter for group
filters[ filterGroup ] = $this.attr('data-filter');
// combine filters
var filterValue = concatValues( filters );
// set filter for Isotope
$grid.isotope({ filter: filterValue });
updateFilterCount();
});
// bind filter on select change
$('.filterstextsearch').on( 'change', '.button-group', function() {
var $this = $(this);
// get group key
var filterGroup = $this.attr('data-filter-group');
// set filter for group
filters[ filterGroup ] = $this.find(':selected').attr('data-filter');
// combine filters
var filterValue = concatValues( filters );
// set filter for Isotope
$grid.isotope({ filter: filterValue });
updateFilterCount();
$(':selected', this).addClass('is-checked').siblings().removeClass('is-checked')
});
// change is-checked class on buttons
$('.button-group').each( function( i, buttonGroup ) {
var $buttonGroup = $( buttonGroup );
$buttonGroup.on( 'click', '.button', function() {
$buttonGroup.find('.is-checked').removeClass('is-checked');
$( this ).addClass('is-checked');
});
});
// flatten object by concatting values
function concatValues( obj ) {
var value = '';
for ( var prop in obj ) {
value += obj[ prop ];
}
return value;
}
// use value of search field to filter
var $quicksearch = $('.isofilter').keyup( debounce( function() {
qsRegex = new RegExp( $quicksearch.val(), 'gi' );
$grid.isotope({
filter: function() {
return qsRegex ? $(this).text().match( qsRegex ) : true;
}
});
updateFilterCount();
}, 200 ) );
function updateFilterCount() {
$filterCount.text( iso.filteredItems.length + ' items' );
}
updateFilterCount();
// 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;
}
timeout = setTimeout( delayed, threshold || 100 );
}
}
});
I suspect the reason it doesn't work is that I use imagesLoaded? In console I get "TypeError: iso is undefined". But I can't figure out how to fix it.

Alright, 2 days later, I managed to find a solution by myself. The problem was because of imagesLoaded. So I replaced the "// init Isotope" portion of the code like this and now the counts are working.
// init Isotope
var $grid = $('.breakdown').isotope({
itemSelector: '.status',
getSortData: {
name: '.elementsort',
},
sortBy : 'name',
});
$grid.imagesLoaded().progress( function() {
$grid.isotope('layout');
});
The issue with this option is that the page is much slower to load now than when I initialized Isotope after all images were loaded. If anyone has a solution to this, it would be much appreciated.

Related

Stop infinite scroll from jumping to end of page when loading content

I have implemented infinite scroll with isotope on my website and I am having a problem with loading posts. The posts load fine, but lets say I scroll down to load more posts but I am still viewing the posts already there, infinite scroll jumps to the bottom of the page whenever new posts are loaded. How can I stop this behavior and maintain the position on the page even when more posts are loaded?
My script --
$(function () {
var selectChoice, updatePageState, updateFiltersFromObject,
$container = $('.isotope');
////////////////////////////////////////////////////////////////////////////////////
/// EVENT HANDLERS
////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////
// Mark filtering element as active/inactive and trigger filters update
$('.js-filter').on( 'click', '[data-filter]', function (event) {
event.preventDefault();
selectChoice($(this), {click: true});
$container.trigger('filter-update');
});
//////////////////////////////////////////////////////
// Sort filtered (or not) elements
$('.js-sort').on('click', '[data-sort]', function (event) {
event.preventDefault();
selectChoice($(this), {click: true});
$container.trigger('filter-update');
});
//////////////////////////////////////////////////////
// Listen to filters update event and update Isotope filters based on the marked elements
$container.on('filter-update', function (event, opts) {
var filters, sorting, push;
opts = opts || {};
filters = $('.js-filter li.active a:not([data-filter="all"])').map(function () {
return $(this).data('filter');
}).toArray();
sorting = $('.js-sort li.active a').map(function () {
return $(this).data('sort');
}).toArray();
if (typeof opts.pushState == 'undefined' || opts.pushState) {
updatePageState(filters, sorting);
}
$container.isotope({
filter: filters.join(''),
sortBy: sorting
});
});
//////////////////////////////////////////////////////
// Set a handler for history state change
History.Adapter.bind(window, 'statechange', function () {
var state = History.getState();
updateFiltersFromObject(state.data);
$container.trigger('filter-update', {pushState: false});
});
////////////////////////////////////////////////////////////////////////////////////
/// HELPERS FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////
// Build an URI to get the query string to update the page history state
updatePageState = function (filters, sorting) {
var uri = new URI('');
$.each(filters, function (idx, filter) {
var match = /^\.([^-]+)-(.*)$/.exec(filter);
if (match && match.length == 3) {
uri.addSearch(match[1], match[2]);
}
});
$.each(sorting, function (idx, sort) {
uri.addSearch('sort', sort);
});
History.pushState(uri.search(true), null, uri.search() || '?');
};
//////////////////////////////////////////////////////
// Select the clicked (or from URL) choice in the dropdown menu
selectChoice = function ($link, opts) {
var $group = $link.closest('.btn-group'),
$li = $link.closest('li'),
mediumFilter = $group.length == 0;
if (mediumFilter) {
$group = $link.closest('.js-filter');
}
if (opts.click) {
$li.toggleClass('active');
} else {
$li.addClass('active');
}
$group.find('.active').not($li).removeClass('active');
if (!mediumFilter) {
if ($group.find('li.active').length == 0) {
$group.find('li:first-child').addClass('active');
}
$group.find('.selection').html($group.find('li.active a').first().html());
}
};
//////////////////////////////////////////////////////
// Update filters by the values in the current URL
updateFiltersFromObject = function (values) {
if ($.isEmptyObject(values)) {
$('.js-filter').each(function () {
selectChoice($(this).find('li').first(), {click: false});
});
selectChoice($('.js-sort').find('li').first(), {click: false});
} else {
$.each(values, function (key, val) {
val = typeof val == 'string' ? [val] : val;
$.each(val, function (idx, v) {
var $filter = $('[data-filter=".' + key + '-' + v + '"]'),
$sort = $('[data-sort="' + v + '"]');
if ($filter.length > 0) {
selectChoice($filter, {click: false});
} else if ($sort.length > 0) {
selectChoice($sort, {click: false});
}
});
});
}
};
////////////////////////////////////////////////////////////////////////////////////
/// Initialization
////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////
// Initialize Isotope
$container.imagesLoaded( function(){
$container.isotope({
masonry: { resizesContainer: true },
itemSelector: '.item',
getSortData: {
date: function ( itemElem ) {
var date = $( itemElem ).find('.thedate').text();
return parseInt( date.replace( /[\(\)]/g, '') );
},
area: function( itemElem ) { // function
var area = $( itemElem ).find('.thearea').text();
return parseInt( area.replace( /[\(\)]/g, '') );
},
price: function( itemElem ) { // function
var price = $( itemElem ).find('.theprice').text();
return parseInt( price.replace( /[\(\)]/g, '') );
}
}
});
var total = $(".next a:last").html();
var pgCount = 1;
var numPg = total;
// jQuery InfiniteScroll Plugin.
$container.infinitescroll({
contentSelector : '.isotope',
speed : 'fast',
behavior: 'simplyrecipes',
navSelector : '#pagi', // selector for the paged navigation
nextSelector : '#pagi a.next', // selector for the NEXT link (to page 2)
itemSelector : '.item', // selector for all items you'll retrieve
animate: true,
debug: true,
loading: {
selector: '#infscr-loading',
finishedMsg: 'No more content to load.'
}
},
// Trigger Masonry as a callback.
function( newElements ) {
pgCount++;
if(pgCount == numPg) {
$(window).unbind('.infscr');
$container.isotope('reload');
$container.append( newElements ).isotope( 'appended', newElements, true );
$('#infscr-loading').find('em').text('No more content to load.');
$('#infscr-loading').animate({
opacity: 1
}, 200);
setTimeout(function() {
$('#infscr-loading').animate({
opacity: 0
}, 300);
});
} else {
loadPosts(newElements);
}
});
});
function loadPosts(newElements) {
// Hide new posts while they are still loading.
var newElems = $( newElements ).css({ opacity: 0 });
// Ensure that images load before adding to masonry layout.
newElems.imagesLoaded(function() {
// Show new elements now that they're loaded.
newElems.animate({ opacity: 1 });
$container.isotope( 'appended', newElems, true );
});
}
//////////////////////////////////////////////////////
// Initialize counters
$('.stat-count').each(function () {
var $count = $(this),
filter = $count.closest('[data-filter]').data('filter');
$count.html($(filter).length);
});
//////////////////////////////////////////////////////
// Set initial filters from URL
updateFiltersFromObject(new URI().search(true));
$container.trigger('filter-update', {pushState: false});
});
});
You can start by overriding the default animate property of infinite-scroll
$container.infinitescroll({
animate:false, //this does just that
});
And about your dublicate entry for every request (or perhaps some requests ),
It has something to do with your backend i.e the way you are offseting the data
Infinite Scroll works great with page numbers i.e it
Sends 2,3,4,5,6 ... So For every request its sends that request.
I think you are using page offset instead , and you will probably end up with duplicate entries .
If you are using php as your receiving end ... this might help
//In your Controller
$temp = ( $page_offset * $per_page );
$offset = $temp - $per_page ;
//In your model
$this->db->query("SELECT * FROM GALLERY OFFSET $offset limit 10");
This is just a rough idea , I hope it helps.
If it does , Care to Check out Sticky Bubble ? - A game available for free on Google Play .
Cheers :)

selectize.js questions about a no result plugin and allowing default behaviour on link

I am using selectize for an auto suggest field, and i have used one of the no_results plugins to show a link when there is no result, and this works great for the most part, but i have a few dramas i am just not sure how to get around
I have two things i need to get help with
1st most important - How to pass variables to the plugin
I have multiple instances of selectize on the several pages, so i need to pass the vars hr_link and hr_label to the plugin so i don't have to recreate the plugin 30 times with just the those vars different
2nd - Allow link to be clicked, bypassing default behaviour
To get the links to be clickable i have used the onmousedown() and touchstart() but is there a better way to re-enable the default click on just this link in the results box.
I have spent a lot of time researching these items, so I don't think it is a duplicate
// The Plugin
Selectize.define('header_no_results', function( options ) {
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
var ignoreKeys = [KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN];
var self = this;
var hr_link = 'http://link_to_info.com';
var hr_label = 'country';
options = $.extend({
message: ' No results found: click here to add a'+hr_label,
html: function(data) {
return '<div class="selectize-dropdown-content">' + data.message + '</div>';
}
}, options );
self.on('type', function() {
var message = 'Not Found: click here Add a '+hr_label;
if (!self.hasOptions) {
self.$empty_results_container.html(message).show();
} else {
self.$empty_results_container.hide();
}
});
self.onKeyUp = (function() {
var original = self.onKeyUp;
return function ( e ) {
if (ignoreKeys.indexOf(e.keyCode) > -1) return;
self.isOpen = false;
original.apply( self, arguments );
}
})();
self.onBlur = (function () {
var original = self.onBlur;
return function () {
original.apply( self, arguments );
self.$empty_results_container.hide();
};
})();
self.setup = (function() {
var original = self.setup;
return function() {
original.apply( self, arguments);
self.$empty_results_container = $(
options.html($.extend({
classNames: self.$input.attr( 'class' )
}, options))
);
self.$empty_results_container.hide();
self.$dropdown.append(self.$empty_results_container);
};
})();
});
// the function calling the plugin
$('#companyLinks').selectize({
valueField: 'id',
labelField: 'display',
searchField: 'display',
maxItems: 1,
options: [],
create: false,
onItemAdd: function(value){
window.location.href = 'http://my_link.com/'+value;
},
load: function(query, callback) {
if (!query.length) return callback();
$.ajax({
url: 'http://link.com/get/list',
type: 'GET',
dataType: 'json',
data: {
q: query
},
error: function() {
callback();
},
success: function(res) {
callback(res);
//window.open($(res).val(), '_self');
}
});
},
plugins: ['header_no_results']
});
The solution to pass the var was not all that hard after all just had to look in the right place, and there is further info here
In the function we need to change out
plugins: ['header_no_results']
with
plugins: { "header_no_results": {
link : "page/location",
} }
then we can retrieve link and declare the var we needed in the plugin by
var hr_link = options.link;

Filters + Search with Isotopes Breaks Search?

I am using Isotopes (v1) and have created a search field following an example in a Pen.
Initially it works, however, if I filter the Isotope gallery then the search field stops working.
I believe the search function still runs just doesn't filter the gallery and I am unsure how to fix the problem. In fact I am unsure what the exact problem is as no errors are thrown.
Here is a Fiddle with a working example.
Here is the search, filter and isotope JavaScript:
var $container = $('.isotope'),
qsRegex,
filters = {};
$container.isotope({
itemSelector : '.element',
masonry : {
columnWidth : 120
},
getSortData : {
name : function ( $elem ) {
return $elem.find('.name').text();
}
},
filter: function() {
return qsRegex ? $(this).text().match( qsRegex ) : true;
}
});
function searchFilter() {
qsRegex = new RegExp( $quicksearch.val(), 'gi' );
$container.isotope();
}
// use value of search field to filter
var $quicksearch = $('#quicksearch').keyup( debounce( searchFilter ) );
$('#reset').on( 'click', function() {
$quicksearch.val('');
searchFilter()
});
// store filter for each group
$('#filters').on( 'click', '.button', function() {
var $this = $(this);
// get group key
var $buttonGroup = $this.parents('.button-group');
var filterGroup = $buttonGroup.attr('data-filter-group');
// set filter for group
filters[ filterGroup ] = $this.attr('data-filter');
// combine filters
var filterValue = '';
for ( var prop in filters ) {
filterValue += filters[ prop ];
}
// set filter for Isotope
$container.isotope({ filter: filterValue });
});
// 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 );
}
}
How do I solve the problem?
Note: I am using jQuery 2.1.1.
In you example $('#filters').on('click', '.button', function () stoping the search function and you reset buton placed inside #filters div so when you click it search engine is stoped too.
I have not the best solution, but it solve some problems:
Idea in using function to call engine back:
var iso = function() {
//engine here
}
and
$(function () {
iso();
$('.iso').click(function(){
setTimeout(iso, 500);
});
});
without setTimeout it can't work.
But it don't solve the main problem
look at FIDDLE and you'll understand what I mean
Or you just can place reset and Show All buttons outside #filters div
I faced the same problem implementing Filters + Search functionality.
I solved this problem passing the filter function to the Isotope call ($container.isotope();) in the search function (function searchFilter(){...}) instead of when initializing the Isotope instance.
So, in your code it should be like this:
// No filter specified when initializing the Isotope instance
$container.isotope({
itemSelector : '.element',
masonry : {
columnWidth : 120
},
getSortData : {
name : function ( $elem ) {
return $elem.find('.name').text();
}
}
});
// Instead, the filter is specified here
function searchFilter() {
qsRegex = new RegExp( $quicksearch.val(), 'gi' );
$container.isotope({
filter: function() {
return qsRegex ? $(this).text().match( qsRegex ) : true;
}
});
}

Cannot select a dynamically added list item until it is clicked

I have written a small JQuery plugin that creates a dropdown box based on bootstrap. I have written it to where a data attribute supplies a url that produces the list items. After the ajax call, Jquery loops through the list items and inserts them into the dropdown menu. Here is what I do not understand, the plugin takes a div with the class of .combobox and appends the required html to make the combobox. It uses two functions, _create() and _listItems(). _create() actually adds the html and calls on _listItems() to make the ajax call and it returns the list items to be appended. Looks like this:
;(function ( $, window, document, undefined ) {
var Combobox = function(element,options) {
this.$element = $(element);
this.$options = $.extend({}, $.fn.combobox.defaults, options);
this.$html = {
input: $('<input type="text" placeholder="[SELECT]" />').addClass('form-control'),
button: $('<div id="test"/>').addClass('input-group-btn')
.append($('<button />')
.addClass('btn btn-default input-sm')
.append('<span class="caret"></span>'))
}
this.$list_type = this.$element.attr('data-type');
this.$url = this.$element.attr('data-url');
this.$defaultValue = this.$element.attr('data-default');
this._create();
this.$input = this.$element.find('input');
this.$button = this.$element.find('button');
this.$list = this.$element.find('ul')
this.$button.on('click',$.proxy(this._toggleList,this));
this.$element.on('click','li',$.proxy(this._itemClicked,this));
this.$element.on('mouseleave',$.proxy(this._toggleList,this));
if(this.$defaultValue) {
this.selectByValue(this.$defaultValue);
}
}
Combobox.prototype = {
constructor: Combobox,
_create: function() {
this.$element.addClass('input-group input-group-sm')
.append(this.$html.input)
.append(this._listItems())
.append(this.$html.button);
},
_itemClicked: function(e){
this.$selectedItem = $(e.target).parent();
this.$input.val(this.$selectedItem.text());
console.log(this.$element.find('[data-value="W"]'))
this._toggleList(e);
e.preventDefault();
},
_listItems: function() {
var list = $('<ul />').addClass('dropdown-menu');
$.ajax({
url: this.$url,
type: 'POST',
data: {opt: this.$list_type},
success:function(data){
$.each(data,function(key,text){
list.append($('<li class="listObjItem" data-value="'+text.id+'">'+text.value+'</li>'));
})
}
})
return list
},
selectedItem: function() {
var item = this.$selectedItem;
var data = {};
if (item) {
var txt = this.$selectedItem.text();
data = $.extend({ text: txt }, this.$selectedItem.data());
}
else {
data = { text: this.$input.val()};
}
return data;
},
selectByValue: function(value) {
var selector = '[data-value="'+value+'"]';
this.selectBySelector(selector);
},
selectBySelector: function (selector) {
var $item = this.$element.find(selector);
if (typeof $item[0] !== 'undefined') {
this.$selectedItem = $item;
this.$input.val(this.$selectedItem.text());
}
else {
this.$selectedItem = null;
}
},
enable: function () {
this.$input.removeAttr('disabled');
this.$button.children().removeClass('disabled');
this.$button.on('click',$.proxy(this._toggleList,this));
},
disable: function () {
this.$input.attr('disabled', true);
this.$button.children().addClass('disabled');
this.$button.off('click',$.proxy(this._toggleList,this));
},
_toggleList: function(e) {
if(e.type == 'mouseleave') {
if(this.$list.is(':hidden')) {
return false;
} else {
this.$list.hide();
}
} else {
this.$list.toggle();
e.preventDefault();
}
}
}
$.fn.combobox = function (option) {
return this.each(function () {
if (!$.data(this, 'combobox')) {
$.data(this, 'combobox',
new Combobox( this, option ));
}
});
};
$.fn.combobox.defaults = {};
$.fn.combobox.Constructor = Combobox;
})( jQuery, window, document );
The problem is that after the items are appended to the DOM, everything is selectable accept the list items. I currently have an .on() statement that binds the click event with the list item. To test this out I have used console.log(this.$element.find('[data-value="W"]') and it does not return an element, however if I place that same console log in the click callback of the list item it will return the element and it is selectable. Am I doing something wrong?
EDIT
I have pasted the entire plugin to save on confusion.

Backbone model.change() not firing

I'm currently writing an application using Backbone and for some reason, it doesn't update a view, but only in certain circumstances.
If I refresh the page at index.html#/blog/2 it loads the page just fine, everything works great. However, if I refresh the page at index.html#/blog/1 and then change the URL to index.html#/blog/2 and press enter(NOT refresh), the change never gets fired.
This is my router:
makeChange: function() {
// Set activePage to the current page_id => /blog/2
var attributes = {activePage: this.page_id};
var $this = this;
// Loop through all sections in the app
app.sections.some( function( section ) {
// Check if section has the page
if( !section.validate( attributes ) )
{
// If it has, set the activePage to /blog/2
section.set( attributes, {silent: true} );
// Set active section to whatever section-id was matched
app.set( {activeSect: section.id}, {silent: true} );
console.log('Calling change!');
// Calling change on both the app and the section
app.change();
section.change();
console.log('Change complete!');
return true;
}
});
}
This is the app view(which is referenced as "app" up above^):
var AppView = Backbone.View.extend({
initialize: function( option ) {
app.bind( 'change', _.bind( this.changeSect, this ) );
},
changeSect: function() {
var newSect = app.sections.get( app.get('activeSect' ) );
var newSectView = newSect.view;
if( !app.hasChanged( 'activeSect' ) )
newSectView.activate( null, newSect );
else
{
var oldSect = app.sections.get( app.previous( 'activeSect' ) );
var oldSectView = oldSect.view;
newSectView.activate( oldSect, newSect );
oldSectView.deactivate( oldSect, newSect );
}
}
});
Tell me if you need to see some other classes/models/views.
I solved it! This only happens when navigating between different pages(by changing activePage in the section) in the same section, so activeSect in app was never changed, thus never called changeSect(). Now even when activeSect is the same in the app, and activePage has changed in the section, it will call the changeSect() in the app anyway.
In Section-model, I added this:
initialize: function() {
this.pages = new Pages();
this.bind( 'change', _.bind( this.bindChange, this ) );
},
prepareForceChange: function() {
this.forceChange = true;
},
bindChange: function() {
console.log('BINDCHANGE!');
if( this.forceChange )
{
AppView.prototype.forceChange();
this.forceChange = false;
}
},
In router.makeChange() above this:
section.set( attributes, {silent: true} );
app.set( {activeSect: section.id}, {silent: true} );
I added:
var oldSectId = app.get('activeSect');
if( oldSectId == section.id ) section.prepareForceChange();

Categories

Resources