Infinite Scroll (JS) + iPhone - Images not loading - javascript

So I am using the Infinite Scroll script to prevent the user having to move on to the next page to view more products in Woocommerce. Everything works great except...
On iPhone (and iPad) the images that would technically be on the next page do not load at all once "Load More" initiates... so for example, if I have 1-8 products visible and I scroll down - the rest of the products appear, but only the images for the initial 1-8 products load, but the rest do not.
I have tried implementing bug fixes to no avail... just wondering if anyone else has encountered this and managed to find a fix?
<script>
var elem = document.querySelector('.shop-section');
var infScroll = new InfiniteScroll( elem, {
// options
path: '.woocommerce-pagination a.next',
append: '.products',
hideNav: '.woocommerce-pagination',
history: false,
status: '.page-load-status',
});
// iPhone Bug Fix
infScroll.on( 'append', function( response, path, items ) {
for ( var i=0; i < items.length; i++ ) {
reloadSrcsetImgs( item[i] );
}
});
function reloadSrcsetImgs( item ) {
var imgs = item.querySelectorAll('img[srcset]');
for ( var i=0; i < imgs.length; i++ ) {
var img = imgs[i];
img.outerHTML = img.outerHTML;
}
}
</script>
EDIT: I should also add that I noticed an error in the console as follows:
Uncaught ReferenceError: item is not defined
at InfiniteScroll.<anonymous> ((index):970)
at InfiniteScroll.proto.emitEvent (infinite-scroll.js?ver=1:254)
at InfiniteScroll.proto.dispatchEvent (infinite-scroll.js?ver=1:688)
at InfiniteScroll.<anonymous> (infinite-scroll.js?ver=1:1069)
at InfiniteScroll.proto.appendNextPage (infinite-scroll.js?ver=1:1076)
at InfiniteScroll.proto.onPageLoad (infinite-scroll.js?ver=1:1052)
at InfiniteScroll.<anonymous> (infinite-scroll.js?ver=1:1029)
at XMLHttpRequest.req.onload (infinite-scroll.js?ver=1:1249)

Related

JQuery .sortable() only works AFTER page refresh

I have a case of a page containing a lot of JQuery, of which almost all of it works. All of it apart from:
private sortableOptions = {
JSPA: this,
delay: 50,
group: "favourites",
connectWith: ".sel-favourite-groups",
vertical: false,
itemSelector: "app-favourite",
placeholder: "<li class='placeholder favourite list-group-item' style=''></li>",
tolerance: 15,
onDrop: this.sortDrop,
onDragStart: this.sortDragStart
};
$list.sortable(this.sortableOptions);
This however does work if I refresh the page (press f5 etc).
When not working (on first page load) what I see is that list items are movable(you can click and drag them around), the event onDragStart fires, and the event onDrop fires. However while moving them no placeholder is displayed, and the other elements don't shift position. On drop the moving element just returns to its original location.
The relevant code in question in full is:
private render(): JQuery {
let $page = this.getTemplate("home");
let $list = $page.find("#home-favourites");
$list.sortable(this.sortableOptions);
try {
for (let f = 0; f < this.user.UserSettings.favourites.sites.length; f++) {
this.drawFavourite($list, this.user.UserSettings.favourites.sites[f]);
}
// and do the same for the groups
for (let g = 0; g < this.user.UserSettings.favourites.groups.length; g++) {
let group = this.user.UserSettings.favourites.groups[g];
// add the header and list to the DOM
let $list = this.drawFavouriteGroup(group);
$list.sortable(this.sortableOptions);
if (group.state == "minimised") {
let $li = $list.parent();
this.drawMinimisedFavouriteGroup($li, group.name);
$li.addClass("collapse");
}
for (let f = 0; f < group.sites.length; f++) {
this.drawFavourite($list, group.sites[f]);
}
}
}
catch (ex) {
console.log("EXCEPTION:: Home.render(): ", ex);
}
return $page;
}
From reviewing other similar issues (normally that JQuery things DON'T work after a refresh) I have already tried:
Adding everything to the DOM then calling .sortable()
Calling .sortable('refresh')
Simplifying the code so the 'groups' above(multiple <ul />s aren't included
None of these made any difference - never works on first load, always works after page refresh
Also navigating away from the page, and back means the .sortable() doesn't work.
What I can't understand is how a refresh can make it work, shouldn't an initial page load when navigated to - with no cached content - be identical to hitting f5?
As I said at the start, there is A LOT of other jQuery on the page all of which works as expected.
Anyone got any idea what might be going on here?

How to add dynamically items to a Magnific Popup gallery

I use Magnific Popup and Shuffle plugins on my page. I load new items as soon as the user reach the bottom of the page. What I want to do is to bind my new items to Magnific Popup after loading them.
I tried to follow this answer Dynamically add items to Magnific Popup gallery but it is not working for me.
Below the js code I use to do so.
First, once the page is loaded I use this code to setup MP
// gallery mode
$('.gallery-item').magnificPopup({
gallery: {
enabled: true
},
mainClass: 'mfp-fade',
fixedContentPos: false,
type: 'image'
});
Then, as soon as the user reach the bottom of the page if request the JSON file
$.getJSON( "list-dir.php")
.done(function( data ) {
var diff = Object.keys(data).length - $(".item").length;
var NUM_TO_LOAD = diff > 10 ? 10 : diff;
if(Object.keys(data).length > $(".item").length){
for(var i = 0 ; i<= 10; i++) {
var el = data[$(".item").length];
if (el === undefined) { break; }
var name = el.substring(0,el.indexOf("_") );
var $item = $('<div class="item shuffle-item filtered" data-groups="photo" data-groups="photo"><div class="item-img" style="background-image: url(\'img\/dresses\/' + el + '\' )"></div><div class="item-overlay"><h5>'+ name +'</h5></div></div>');
//Shuffle update
$('.portfolio').append($item);
$('.portfolio').shuffle('appended', $item);
// get instance (after popup was opened)
var mfp = $.magnificPopup.instance;
// modify the items array (push/remove/edit)
mfp.gallery.push({
src: $item
});
mfp.updateItemHTML();
}
}
});

Load blocking jQuery animate

I have built a gallery viewer with a preload function.
The preload function is as follows:
$.preloadFullImages = function() {
// Create array of images
var set = [];
$('.slide-item img').each(function() {
var img = $(this).data('src');
var id = $(this).parent().attr('id');
$(this).remove();
set.push([img,id]);
});
// Set current image
var current = 0;
var iterate = function() {
var current_src = set[current][0];
var current_id = set[current][1];
var temp = '<img src="'+current_src+'" />';
var target = '#'+current_id;
var targetImg = '#'+current_id+' img';
// Load 'temp' image
$(temp).bind('load', function() {
// Show image
$(target).append(temp);
$(targetImg).show();
$(this).remove();
});
if ( ++current < set.length ) iterate();
};
iterate();
};
On load of the page, images are loaded sequentially.
The problem is until all the images are loaded, the animation between images (prev and next arrows) is stunted and doesn't work correctly. I want the gallery viewer to transition smoothly between slides (images) even if not all images are loaded.
You can see a live demo here: http://www.davidclapp.net/portfolio
The issue is especially apparent on the iPhone (safari).
Is there a way to ensure the animation is smooth even whilst images are still loading?
Edit: I am using this plugin for CSS3 transitions - http://ricostacruz.com/jquery.transit/
$.preloadFullImages = function(callback) {
...
if ( ++current < set.length ) iterate();
else if(callback) callback(); //check if callback exists, then call it.
};
iterate();
};
These two lines being the important part.
$.preloadFullImages = function(callback) {
if ( ++current < set.length ) iterate();
else callback();
make a function to call the beginning of your animation, pass it to "preloadFullImages"

ReactJS: Modeling Bi-Directional Infinite Scrolling

Our application uses infinite scrolling to navigate large lists of heterogenous items. There are a few wrinkles:
It's common for our users to have a list of 10,000 items and need to scroll through 3k+.
These are rich items, so we can only have a few hundred in the DOM before browser performance becomes unacceptable.
The items are of varying heights.
The items may contain images and we allow the user to jump to a specific date. This is tricky because the user can jump to a point in the list where we need to load images above the viewport, which would push the content down when they load. Failing to handle that means that the user may jump to a date, but then be shifted to an earlier date.
Known, incomplete solutions:
(react-infinite-scroll) - This is just a simple "load more when we hit the bottom" component. It does not cull any of the DOM, so it will die on thousands of items.
(Scroll Position with React) - Shows how to store and restore the scroll position when inserting at the top or inserting at the bottom, but not both together.
I'm not looking for the code for a complete solution (although that would be great.) Instead, I'm looking for the "React way" to model this situation. Is scroll position state or not? What state should I be tracking to retain my position in the list? What state do I need to keep so that I trigger a new render when I scroll near the bottom or top of what is rendered?
This is a mix of an infinite table and an infinite scroll scenario. The best abstraction I found for this is the following:
Overview
Make a <List> component that takes an array of all children. Since we do not render them, it's really cheap to just allocate them and discard them. If 10k allocations is too big, you can instead pass a function that takes a range and return the elements.
<List>
{thousandelements.map(function() { return <Element /> })}
</List>
Your List component is keeping track of what the scroll position is and only renders the children that are in view. It adds a large empty div at the beginning to fake the previous items that are not rendered.
Now, the interesting part is that once an Element component is rendered, you measure its height and store it in your List. This lets you compute the height of the spacer and know how many elements should be displayed in view.
Image
You are saying that when the image are loading they make everything "jump" down. The solution for this is to set the image dimensions in your img tag: <img src="..." width="100" height="58" />. This way the browser doesn't have to wait to download it before knowing what size it is going to be displayed. This requires some infrastructure but it's really worth it.
If you can't know the size in advance, then add onload listeners to your image and when it is loaded then measure its displayed dimension and update the stored row height and compensate the scroll position.
Jumping at a random element
If you need to jump at a random element in the list that's going to require some trickery with scroll position because you don't know the size of the elements in between. What I suggest you to do is to average the element heights you already have computed and jump to the scroll position of last known height + (number of elements * average).
Since this is not exact it's going to cause issues when you reach back to the last known good position. When a conflict happens, simply change the scroll position to fix it. This is going to move the scroll bar a bit but shouldn't affect him/her too much.
React Specifics
You want to provide a key to all the rendered elements so that they are maintained across renders. There are two strategies: (1) have only n keys (0, 1, 2, ... n) where n is the maximum number of elements you can display and use their position modulo n. (2) have a different key per element. If all the elements share a similar structure it's good to use (1) to reuse their DOM nodes. If they don't then use (2).
I would only have two pieces of React state: the index of the first element and the number of elements being displayed. The current scroll position and the height of all the elements would be directly attached to this. When using setState you are actually doing a rerender which should only happen when the range changes.
Here is an example of infinite list using some of the techniques I describe in this answer. It's going to be some work but React is definitively a good way to implement an infinite list :)
have a look at http://adazzle.github.io/react-data-grid/index.html#
This looks like a powerful and performant datagrid with Excel-like features and lazy loading/optimized rendering (for millions of rows) with rich editing features (MIT licenced).
Not yet tried in our project but will do so pretty soon.
A great resource to search for things like these is also http://react.rocks/
In this case, a tag search is helpful:
http://react.rocks/tag/InfiniteScroll
I was facing a similar challenge for modeling single-direction infinite scrolling with heterogeneous item heights and so made an npm package out of my solution:
https://www.npmjs.com/package/react-variable-height-infinite-scroller
and a demo: http://tnrich.github.io/react-variable-height-infinite-scroller/
You can check out the source code for the logic, but I basically followed the recipe #Vjeux outlined in the above answer. I haven't yet tackled jumping to a particular item, but I'm hoping to implement that soon.
Here's the nitty-gritty of what the code currently looks like:
var React = require('react');
var areNonNegativeIntegers = require('validate.io-nonnegative-integer-array');
var InfiniteScoller = React.createClass({
propTypes: {
averageElementHeight: React.PropTypes.number.isRequired,
containerHeight: React.PropTypes.number.isRequired,
preloadRowStart: React.PropTypes.number.isRequired,
renderRow: React.PropTypes.func.isRequired,
rowData: React.PropTypes.array.isRequired,
},
onEditorScroll: function(event) {
var infiniteContainer = event.currentTarget;
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
var currentAverageElementHeight = (visibleRowsContainer.getBoundingClientRect().height / this.state.visibleRows.length);
this.oldRowStart = this.rowStart;
var newRowStart;
var distanceFromTopOfVisibleRows = infiniteContainer.getBoundingClientRect().top - visibleRowsContainer.getBoundingClientRect().top;
var distanceFromBottomOfVisibleRows = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
var rowsToAdd;
if (distanceFromTopOfVisibleRows < 0) {
if (this.rowStart > 0) {
rowsToAdd = Math.ceil(-1 * distanceFromTopOfVisibleRows / currentAverageElementHeight);
newRowStart = this.rowStart - rowsToAdd;
if (newRowStart < 0) {
newRowStart = 0;
}
this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
}
} else if (distanceFromBottomOfVisibleRows < 0) {
//scrolling down, so add a row below
var rowsToGiveOnBottom = this.props.rowData.length - 1 - this.rowEnd;
if (rowsToGiveOnBottom > 0) {
rowsToAdd = Math.ceil(-1 * distanceFromBottomOfVisibleRows / currentAverageElementHeight);
newRowStart = this.rowStart + rowsToAdd;
if (newRowStart + this.state.visibleRows.length >= this.props.rowData.length) {
//the new row start is too high, so we instead just append the max rowsToGiveOnBottom to our current preloadRowStart
newRowStart = this.rowStart + rowsToGiveOnBottom;
}
this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
}
} else {
//we haven't scrolled enough, so do nothing
}
this.updateTriggeredByScroll = true;
//set the averageElementHeight to the currentAverageElementHeight
// setAverageRowHeight(currentAverageElementHeight);
},
componentWillReceiveProps: function(nextProps) {
var rowStart = this.rowStart;
var newNumberOfRowsToDisplay = this.state.visibleRows.length;
this.props.rowData = nextProps.rowData;
this.prepareVisibleRows(rowStart, newNumberOfRowsToDisplay);
},
componentWillUpdate: function() {
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
this.soonToBeRemovedRowElementHeights = 0;
this.numberOfRowsAddedToTop = 0;
if (this.updateTriggeredByScroll === true) {
this.updateTriggeredByScroll = false;
var rowStartDifference = this.oldRowStart - this.rowStart;
if (rowStartDifference < 0) {
// scrolling down
for (var i = 0; i < -rowStartDifference; i++) {
var soonToBeRemovedRowElement = visibleRowsContainer.children[i];
if (soonToBeRemovedRowElement) {
var height = soonToBeRemovedRowElement.getBoundingClientRect().height;
this.soonToBeRemovedRowElementHeights += this.props.averageElementHeight - height;
// this.soonToBeRemovedRowElementHeights.push(soonToBeRemovedRowElement.getBoundingClientRect().height);
}
}
} else if (rowStartDifference > 0) {
this.numberOfRowsAddedToTop = rowStartDifference;
}
}
},
componentDidUpdate: function() {
//strategy: as we scroll, we're losing or gaining rows from the top and replacing them with rows of the "averageRowHeight"
//thus we need to adjust the scrollTop positioning of the infinite container so that the UI doesn't jump as we
//make the replacements
var infiniteContainer = React.findDOMNode(this.refs.infiniteContainer);
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
var self = this;
if (this.soonToBeRemovedRowElementHeights) {
infiniteContainer.scrollTop = infiniteContainer.scrollTop + this.soonToBeRemovedRowElementHeights;
}
if (this.numberOfRowsAddedToTop) {
//we're adding rows to the top, so we're going from 100's to random heights, so we'll calculate the differenece
//and adjust the infiniteContainer.scrollTop by it
var adjustmentScroll = 0;
for (var i = 0; i < this.numberOfRowsAddedToTop; i++) {
var justAddedElement = visibleRowsContainer.children[i];
if (justAddedElement) {
adjustmentScroll += this.props.averageElementHeight - justAddedElement.getBoundingClientRect().height;
var height = justAddedElement.getBoundingClientRect().height;
}
}
infiniteContainer.scrollTop = infiniteContainer.scrollTop - adjustmentScroll;
}
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
if (!visibleRowsContainer.childNodes[0]) {
if (this.props.rowData.length) {
//we've probably made it here because a bunch of rows have been removed all at once
//and the visible rows isn't mapping to the row data, so we need to shift the visible rows
var numberOfRowsToDisplay = this.numberOfRowsToDisplay || 4;
var newRowStart = this.props.rowData.length - numberOfRowsToDisplay;
if (!areNonNegativeIntegers([newRowStart])) {
newRowStart = 0;
}
this.prepareVisibleRows(newRowStart , numberOfRowsToDisplay);
return; //return early because we need to recompute the visible rows
} else {
throw new Error('no visible rows!!');
}
}
var adjustInfiniteContainerByThisAmount;
//check if the visible rows fill up the viewport
//tnrtodo: maybe put logic in here to reshrink the number of rows to display... maybe...
if (visibleRowsContainer.getBoundingClientRect().height / 2 <= this.props.containerHeight) {
//visible rows don't yet fill up the viewport, so we need to add rows
if (this.rowStart + this.state.visibleRows.length < this.props.rowData.length) {
//load another row to the bottom
this.prepareVisibleRows(this.rowStart, this.state.visibleRows.length + 1);
} else {
//there aren't more rows that we can load at the bottom so we load more at the top
if (this.rowStart - 1 > 0) {
this.prepareVisibleRows(this.rowStart - 1, this.state.visibleRows.length + 1); //don't want to just shift view
} else if (this.state.visibleRows.length < this.props.rowData.length) {
this.prepareVisibleRows(0, this.state.visibleRows.length + 1);
}
}
} else if (visibleRowsContainer.getBoundingClientRect().top > infiniteContainer.getBoundingClientRect().top) {
//scroll to align the tops of the boxes
adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().top - infiniteContainer.getBoundingClientRect().top;
// this.adjustmentScroll = true;
infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
} else if (visibleRowsContainer.getBoundingClientRect().bottom < infiniteContainer.getBoundingClientRect().bottom) {
//scroll to align the bottoms of the boxes
adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
// this.adjustmentScroll = true;
infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
}
},
componentWillMount: function(argument) {
//this is the only place where we use preloadRowStart
var newRowStart = 0;
if (this.props.preloadRowStart < this.props.rowData.length) {
newRowStart = this.props.preloadRowStart;
}
this.prepareVisibleRows(newRowStart, 4);
},
componentDidMount: function(argument) {
//call componentDidUpdate so that the scroll position will be adjusted properly
//(we may load a random row in the middle of the sequence and not have the infinte container scrolled properly initially, so we scroll to the show the rowContainer)
this.componentDidUpdate();
},
prepareVisibleRows: function(rowStart, newNumberOfRowsToDisplay) { //note, rowEnd is optional
//setting this property here, but we should try not to use it if possible, it is better to use
//this.state.visibleRowData.length
this.numberOfRowsToDisplay = newNumberOfRowsToDisplay;
var rowData = this.props.rowData;
if (rowStart + newNumberOfRowsToDisplay > this.props.rowData.length) {
this.rowEnd = rowData.length - 1;
} else {
this.rowEnd = rowStart + newNumberOfRowsToDisplay - 1;
}
// var visibleRows = this.state.visibleRowsDataData.slice(rowStart, this.rowEnd + 1);
// rowData.slice(rowStart, this.rowEnd + 1);
// setPreloadRowStart(rowStart);
this.rowStart = rowStart;
if (!areNonNegativeIntegers([this.rowStart, this.rowEnd])) {
var e = new Error('Error: row start or end invalid!');
console.warn('e.trace', e.trace);
throw e;
}
var newVisibleRows = rowData.slice(this.rowStart, this.rowEnd + 1);
this.setState({
visibleRows: newVisibleRows
});
},
getVisibleRowsContainerDomNode: function() {
return this.refs.visibleRowsContainer.getDOMNode();
},
render: function() {
var self = this;
var rowItems = this.state.visibleRows.map(function(row) {
return self.props.renderRow(row);
});
var rowHeight = this.currentAverageElementHeight ? this.currentAverageElementHeight : this.props.averageElementHeight;
this.topSpacerHeight = this.rowStart * rowHeight;
this.bottomSpacerHeight = (this.props.rowData.length - 1 - this.rowEnd) * rowHeight;
var infiniteContainerStyle = {
height: this.props.containerHeight,
overflowY: "scroll",
};
return (
<div
ref="infiniteContainer"
className="infiniteContainer"
style={infiniteContainerStyle}
onScroll={this.onEditorScroll}
>
<div ref="topSpacer" className="topSpacer" style={{height: this.topSpacerHeight}}/>
<div ref="visibleRowsContainer" className="visibleRowsContainer">
{rowItems}
</div>
<div ref="bottomSpacer" className="bottomSpacer" style={{height: this.bottomSpacerHeight}}/>
</div>
);
}
});
module.exports = InfiniteScoller;

javascript crashing iPad browser

I have some javascript inside a function that creates and populates an image carousel. It works fine after activating it in a pop up window the first 5 or 6 times, but then it eventually crashes the browser. I think there's some kind of leak, like something inside of it needs to be deleted before it gets created again. I know it's the carousel because if I get rid of that part of the script, it no longer crashes.
Here's the carousel script:
/* carousel */
var carousel,
el,
i,
page,
slides;
carousel = new SwipeView('#wrapper', {
numberOfPages: slides.length,
hastyPageFlip: true
});
// Load initial data
for (i=0; i<3; i++) {
page = i==0 ? slides.length-1 : i-1;
el = document.createElement('span');
el.innerHTML = slides[page];
carousel.masterPages[i].appendChild(el)
}
carousel.onFlip(function () {
var el,
upcoming,
i;
for (i=0; i<3; i++) {
upcoming = carousel.masterPages[i].dataset.upcomingPageIndex;
if (upcoming != carousel.masterPages[i].dataset.pageIndex) {
el = carousel.masterPages[i].querySelector('span');
el.innerHTML = slides[upcoming];
}
}
});
This script runs every time I click a link that launches a floating window.
I found out that I needed to clear my wrapper div. In the beginning of my function call:
document.getElementById('wrapper').innerHTML = "";
Seems to work.

Categories