Infinite Scroll history breaks the URL when using JSON - infinite-scroll

I'm using the Infinite Scroll plugin for my products listings. The plugin is loading the data from a JSON and it's working fine if I don't enable the history.
Now I would like the plugin to change the history, so when the user opens a product detail page and then uses the back button he / she gets redirected to the last scroll position.
A problem occurs when I enable the history: 'replace' option that changes the page URL for the subfolder and path of my server-side script breaking all links and images.
Here is my init code:
$infiniteContainer = $('#resultsContainer').infiniteScroll({
path: function() {
return 'ajax/ajax.items.php?'
+ '&s=' + searchStr
+ '&cat=' + filterCat
+ '&sub=' + filterSubCat
+ '&order=' + orderBy
+ '&offset=' + this.pageIndex;
},
responseType: 'text',
history: 'replace',
loadOnScroll: true,
checkLastPage: true
});
when I enable the history then my URL gets converted to:
mydomain.com/ajax/ajax.items.php?s=&cat=&sub=&order=&offset=1
and the URL should stay as mydomain.com/
How can I prevent infinite scroll to change the URL to the wrong path and just passing the variables I need for scrolling to a specific page?

When a new page is loaded from a URL specified by the path function, that URL replaces the current URL displayed in the address bar.
This is done in the setHistory function of the infinite-scroller (see line 1159 in infinite-scroll.pkgd.js).
If your goal is to load data from mydomain.com/ajax/ajax.items.php?s=&cat=&sub=&order=&offset=1 but have mydomain.com?s=&cat=&sub=&order=&offset=1 displayed in the address bar instead, you can modify that function.I didn't find a way to configure behaviour of setHistory, so the following workaround replaces it (for the single InfiniteScroll instance infScroll):
const infScroll = new InfiniteScroll( '#resultsContainer', path: function() {
return 'ajax/ajax.items.php?'
+ '&s=' + searchStr
+ '&cat=' + filterCat
+ '&sub=' + filterSubCat
+ '&order=' + orderBy
+ '&offset=' + this.pageIndex;
},
responseType: 'text',
history: 'replace',
loadOnScroll: true,
checkLastPage: true
});
const originalSetHistory = infScroll.setHistory;
infScroll.setHistory = function (title, path) {
const modifiedPath = path.replace("mydomain.com/ajax/ajax.items.php", "mydomain.com")
originalSetHistory.call(infScroll, originalSetHistory, modifiedPath);
}

Related

Meteor/Blaze/Mongo/Leaflet - Dynamically filled Leaflet popups do not pass data to button for entry into database?

I'm working with a Leaflet map that displays marker data based on a MongoDB query. The query results are saved into a variable (I know, bad form for large volumes of info but okay if you only have ~25 pieces) as an array, and then I've iterated over that variable and it's stored information using a for loop to create my leaflet map markers and populate their popups with the information specific to each entry. This part works great.
this.autorun(function(){
fsqresults = FsqResults.find().fetch({});
container = $('<div />');
for (i=0; i < fsqresults.length; i++) {
marker = L.marker([fsqresults[i].geometry.coordinates[1], fsqresults[i].geometry.coordinates[0]], {icon: violetIcon}).addTo(mymap);
container.html("<b>" + "Name: " + "</b>" + fsqresults[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults[i].properties.address + "<br>" +
"<b>" + "Checkins: " + "</b>" + fsqresults[i].properties.checkIns + "<br>" +
"<b>" + "Users: " + "</b>" + fsqresults[i].properties.usersCount + "<br>" +
"<b>" + "Tips: " + "</b>" + fsqresults[i].properties.tips + "<br>");
marker.bindPopup(container[0]);
} // end for loop
For each marker, there is a button to log a "checkin" event to another Mongo collection to house the checkin entries. The button fires an event successfully, and creates the entry into the second database, but will not bind the dynamically populated data to the entry so I can see which marker the user has clicked on.
container.append($('<button class="btn btn-sm btn-outline-primary js-checkin">').text("Check In"));
container.on('click', '.js-checkin', function() {
var currentVenue = fsqresults[i].properties.name;
console.log(currentVenue);
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({user: Meteor.user(), name: currentVenue});
}
});
}); // end this.autorun
The console tells me that currentVenue is undefined. I know this has something to do with the fact that fsqresults is a dynamically populated variable. I have tried to find ways to "solidify" the information in it (i.e. - creating a second variable with an empty array, pushing the data from fsqresults into it, and then having the markers iterate over that variable) but that hasn't worked as the MongoDB query results, despite being in an array format themselves, will not push or concat into the variable with an empty array successfully.
I've been searching for an answer to this problem and I'm coming up short. I'm lost; is there any other solution which could be staring me in the face?
Some things to note: All of this code lives in the Template.map.onRendered() function. Leaflet has scoping issues if I delegate the code into helpers and events, which is why I haven't created a {markers} template and just done {{#each markers}} over it for iteration. Therefore I am relegated to jQuery style coding for creating DOM elements and firing event triggers. The code above is wrapped in a this.autorun function to ensure it does indeed run upon map rendering. I don't think this is the issue (although one can never rule it out!).
As pointed out in the question comments, you have a scope issue with your i index iterator, and there should be no technical problem in integrating Leaflet with Meteor (although with Blaze that may not be totally trivial nor interesting).
1. Iteration scope issue
The console tells me that currentVenue is undefined.
That is because you try to access fsqresults[i].properties.name in your container.on('click' event listener / callback, which will be called on user click, i.e. after your for loop is complete, hence your i index iterator variable will be equal to fsqresults.length.
You are in the case of Example 6 of the accepted answer of: How do JavaScript closures work?
2. Leaflet integration with Meteor (Blaze)
Since you mention having tried helpers, events, and {{#each markers}}, I assume you use Blaze as your Meteor rendering engine.
While React-Leaflet and Vue2Leaflet indeed offer the possibility to use a kind of "<Marker>" component (same for other types of Leaflet Layer), the latter is only for template declaration purpose, i.e. it does not directly render any DOM / HTML, but only calls some Leaflet methods, which will be in charge of manipulating the DOM. As stated on React-Leaflet limitations:
The components exposed are abstractions for Leaflet layers, not DOM elements.
Side note: interesting to see that angular-leaflet-directive and #asymmetrik/ngx-leaflet did not fall into the same temptation and sticked to JS declaration of Leaflet layers.
Therefore trying to create a Template.Marker (used as {{> Marker}}) in Blaze might be overkill, as you would basically just call some Leaflet factories (like L.marker) within your Template.Marker.onCreated (and needing to access somehow the parent map object to add your Marker into…), without rendering any DOM node yourself (i.e. you would have an HTML file with empty <template name="Marker"></template>).
While we forget about a Marker template in Blaze (as you have already done), we can still leverage Blaze events management to handle user clicks in your Leaflet Popup. For that, we would need a few Blaze features, that I admit could benefit being better documented:
We can attach arbitrary JS data to our template instance.
Template events are delegated, hence we do not need to attach them to each <button> before hand.
We can easily access the template instance in event handlers (as the 2nd argument of the event listener).
2.1. Attaching arbitrary JS data to our template instance
As stated in the Template Instances API:
[…] you can assign additional properties of your choice to the object. Use the onCreated and onDestroyed methods to add callbacks performing initialization or clean-up on the object.
Therefore you can store your fsqresults on your Template instance, so that you can access it later on (typically in your event listener):
Template.myTemplate.onRendered(function () {
this.autorun(() => { // Using an arrow function to keep the same `this`, but you could do `const self = this` beforehand.
const fsqresults = this.fsqresults = FsqResults.find().fetch();
});
});
But since we want to access specific Features later on, it might be more interesting to convert fsqresults to a dictionary. Since your ID seems to be feature.properties.name, you could do:
Template.myTemplate.onCreated(function () {
this.autorun(() => {
const fsqresults = this.fsqresults = FsqResults.find().fetch();
const markersDict = this.markersDict = {};
L.geoJSON(fsqresults, {
pointToLayer(feature, latlng) {
const props = feature.properties;
const markerName = props.name;
// Save a direct reference to the Feature data,
// using the `markerName` as key (ID).
markersDict[markerName] = feature;
// Store the `markerName` in the button `dataset`
// (i.e. as a `data-` attribute),
// as already suggested in the question comments,
// so that we can easily retrieve the ID / key
// of the Marker data associated with the button the user clicked on.
return L.marker(latlng).bindPopup(`
<p>${markerName}</p>
<button role="popupClick" data-marker-name="${markerName}">
Popup action
</button>
`);
},
});
});
});
2.2. Template event handler delegation
As stated on the Blaze Overview Details:
DOM engine […] which features […] event delegation
(sorry there does not seem to be any other mention of this feature in the official doc… let me know if you find a better one!)
Therefore, as long as we create a Template event handler with the appropriate selector, we do not need to attach the event listener on each button, which anyway we may not create as a Node but leave it as String passed to Leaflet .bindPopup (as done in the above code sample).
For example:
Template.myTemplate.events({
// Even if the `<button role="popupClick">` are not DOM nodes yet
// (because Leaflet will create them from the HTML String
// only when the user opens the popup by clicking on the Marker),
// the "click" event will bubble up to the template instance,
// which will call this event handler if it matches the selector.
'click button[role="popupClick"]'() {
console.log('clicked on a button that has been built in a Leaflet Popup');
}
});
2.3. Access the template instance, and our Feature data
The Blaze event handler are called with an extra 2nd argument, which is the current template instance:
The handler function receives two arguments: event, an object with information about the event, and template, a template instance for the template where the handler is defined.
Therefore in our case we can easily retrieve the markersDict variable that we have defined in onCreated, and use it to retrieve the exact Marker's Feature data associated with the button the user clicked on:
Template.myTemplate.events({
'click button[role="popupClick"]'(event, templateInstance) {
const button = event.currentTarget;
const markerName = button.dataset.markerName;
const markerFeature = templateInstance.markersDict[markerName];
// Do something with `markerFeature`…
console.log(markerFeature);
}
});
If you only need the property name, then you could even skip step 2.1 and directly use the markerName string retrieved from the <button> dataset.
I've come up with a solution to the first part of my issue - at first a javascript closure/scope issue to the inner and outer function scopes. I spent about 2 days wrapping my head around this SO answer: the concept of using the first for loop to produce individual instances of the function (if this were a play, the first for loop would "set the stage" for the show), and using the second for loop to execute each instance of the function ("lights, camera, action!").
I also decided that I could maintain scope if I declared my variables inside the first for loop - but I still had this issue of it only pulling the last value. Then I tried simply redeclaring my variables as constants. To my surprise, using const allowed me to write each instance to each map marker, and I could reliably access the correct iteration of the data upon each correspondent map marker! So no need for a second for loop.
this.autorun(function(){
fsqresults_fetch = FsqResults.find().fetch({});
// console.log(fsqresults_fetch);
for (i = 0; i < fsqresults_fetch.length; i++) {
container = $('<div />');
const fsq_marker = L.marker([fsqresults_fetch[i].geometry.coordinates[1], fsqresults_fetch[i].geometry.coordinates[0]], {icon: blueIcon}).addTo(mymap);
const fsq_venueAddress = fsqresults_fetch[i].properties.address;
const fsq_venueName = fsqresults_fetch[i].properties.name;
const fsq_geometry = {type: "Point",
coordinates: [fsqresults_fetch[i].geometry.coordinates[0], fsqresults_fetch[i].geometry.coordinates[1]]};
container.html("<b>" + "Name: " + "</b>" + fsqresults_fetch[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults_fetch[i].properties.address + "<br>");
container.append($('<button class="btn btn-sm btn-outline-primary" id="js-checkin">').text("Check In"));
fsq_marker.bindPopup(container[0]);
container.on('click', '#js-checkin', function() {
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({type: "Feature", geometry: fsq_geometry, properties: {name: fsq_venueName, address: fsq_venueAddress, user: Meteor.user()}});
}
}); //end container.on
} //end for loop
}); //end this.autorun
As I said in the comment on the last response, it's a bit hack-y, but functional enough to do the job successfully.
Now what I'm really curious to try is the solution that #ghybs posted so I have my events grouped and firing as Blaze is supposed to work!

Mapbox GL Popup .setDOMContent example

I'm trying to create a customized button to appear on a pop up which generates a dynamic link (a URL). I don't seem to be able to do this via the .setHTML because of the timing, can't bind a button to a function at runtime. So I thought I'd try the newish .setDOMContent
There's zero information online as to how this feature works. I'm wondering if anyone has an example of this where a button is added to the popup that can run a function and send data.
Here's my very poor attempt at setting this up.
This function creates the popup
function GameObjectPopup(myObject) {
var features = map.queryRenderedFeatures(myObject.point, {
layers: ['seed']
});
if (!features.length) {
return;
}
var feature = features[0];
// Populate the popup and set its coordinates
// based on the feature found.
var popup = new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(ClickedGameObject(feature))
.setDOMContent(ClickedGameObject2(feature))
.addTo(map);
};
This function adds the html via the .setHTML
function ClickedGameObject(feature){
console.log("clicked on button");
var html = '';
html += "<div id='mapboxgl-popup'>";
html += "<h2>" + feature.properties.title + "</h2>";
html += "<p>" + feature.properties.description + "</p>";
html += "<button class='content' id='btn-collectobj' value='Collect'>";
html += "</div>";
return html;
}
This function wants to add the DOM content via the .setDOMContent
function ClickedGameObject2(feature){
document.getElementById('btn-collectobj').addEventListener('click', function()
{
console.log("clicked a button");
AddGameObjectToInventory(feature.geometry.coordinates);
});
}
I'm trying to pipe the variable from features.geometry.coordinates into the function AddGameObjectToInventory()
the error I'm getting when clicking on an object (so as popup is being generated)
Uncaught TypeError: Cannot read property 'addEventListener' of null
Popup#setHTML takes a string that represents some HTML content:
var str = "<h1>Hello, World!</h1>"
popup.setHTML(str);
while Popup#setDOMContent takes actual HTML nodes. i.e:
var h1 = document.createElement('h1');
h1.innerHTML="Hello, World";
popup.setDOMContent(h1);
both of those code snippets would result in identical Popup HTML contents. You wouldn't want to use both methods on a single popup because they are two different ways to do the same thing.
The problem in the code you shared is that you're trying to use the setDOMContent to add an event listener to your button, but you don't need to access the Popup object to add the event listener once the popup DOM content has been added to the map. Here is a working version of what I think you're trying to do: https://jsfiddle.net/h4j554sk/

Ionic router: determining how a view has been entered

I am using Ionic v1.0.0-rc.2.
As controller + views are cached in Ionic, one has to take care to properly initialize the controller $scope.
I am using the following callback to do this:
$scope.$on('$ionicView.beforeEnter', function(){
...
}
However, to know how to initialize the $scope I need to know how the view was called:
Was it called by a back button (A --> B --> back to A)
in this case I want to leave most items as is
Was it returned to after pressing a back button (A --> B --> back to A --> return to B)
in this case I want to leave most items as is
Was it entered following a new path (not back or forward) ... note that the the view may have already been instantiated from previous navigations
in this case I want to re-initialize the view
I have been looking at the documentation below, but could not find a way of doing this, or what parameters, if any, are passed into the callback function. Any documentation pointers would be great.
http://ionicframework.com/docs/api/directive/ionView/
http://blog.ionic.io/navigating-the-changes/
To determine what's going on in routes I chain this:
// Add state change hooks to log issues to console
.run(['$rootScope', '$state', '$urlMatcherFactory', function($rootScope, $state, $urlMatcherFactory) {
$rootScope.$state = $state;
function message(to, toP, from, fromP) { return from.name + angular.toJson(fromP) + " -> " + to.name + angular.toJson(toP); }
$rootScope.$on("$stateChangeStart", function(evt, to, toP, from, fromP) { console.log("Start: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeSuccess", function(evt, to, toP, from, fromP) { console.log("Success: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeError", function(evt, to, toP, from, fromP, err) { console.log("Error: " + message(to, toP, from, fromP), err); });
}])
This should get you going in the right direction with some customization.

Souncloud soundmanager multiple playlist

Hi i'm using SoundCloud manager, http://www.schillmania.com/projects/soundmanager2/ to play audio files on my website. Following their documentation over here: http://soundcloud-sm2.heroku.com/docs/application.html , i'm able to play a sound cloud playist from a set url.
Now I have multiple sets/ playlist, how can I instantiate a new sound manager class and toggle between them by hidden their elements using css.
Here is the sound manager application code( ideally I would want to create two soundmanager.Createsound())
var consumer_key = "59f0360dads135bd07sdasasdadsad8acc14ae2d929",
url = "https://soundcloud.com/artist-2/sets/owl";
// Resolve the given url and get the full JSON-worth of data from SoundCloud regarding the playlist and the tracks within.
$.getJSON('http://api.soundcloud.com/resolve?url=' + url + '&format=json&consumer_key=' + consumer_key + '&callback=?', function(playlist){
// I like to fill out the player by passing some of the data from the first track.
// In this case, you'll just want to pass the first track's title.
$('.title').text(playlist.tracks[0].title);
// Loop through each of the tracks
$.each(playlist.tracks, function(index, track) {
// Create a list item for each track and associate the track *data* with it.
$('<li>' + track.title + '</li>').data('track', track).appendTo('.tracks');
// * Get appropriate stream url depending on whether the playlist is private or public.
// * If the track includes a *secret_token* add a '&' to the url, else add a '?'.
// * Finally, append the consumer key and you'll have a working stream url.
url = track.stream_url;
(url.indexOf("secret_token") == -1) ? url = url + '?' : url = url + '&';
url = url + 'consumer_key=' + consumer_key;
// ## SoundManager2
// **Create the sound using SoundManager2**
soundManager.createSound({
// Give the sound an id and the SoundCloud stream url we created above.
id: 'track_' + track.id,
url: url,
// On play & resume add a *playing* class to the main player div.
// This will be used in the stylesheet to hide/show the play/pause buttons depending on state.
onplay: function() {
$('.player').addClass('playing');
$('.title').text(track.title);
},
onresume: function() {
$('.player').addClass('playing');
},
// On pause, remove the *playing* class from the main player div.
onpause: function() {
$('.player').removeClass('playing');
},
// When a track finished, call the Next Track function. (Declared at the bottom of this file).
onfinish: function() {
nextTrack();
}
});
});
});

jQuery - Refresh Contents of a DIV

I have a form in a jQuery popup on a webpage. The jQuery popup is a div named .vote-form and the form inside it has the name "#form".
When the form is submitted, the content inside the jQuery popup changes to a success message. I need to make it so that when the jQuery popup is closed, the success message is removed and the form is refreshed back to the original form, so that when the jQuery popup is opened again, the form is showing again and NOT the success message.
My feeble attempt to get this result involved refreshing the ENTIRE page when the jQuery popup is closed. This PARTLY has the desired result, but when the page is refreshed, most browsers get a popup asking if the user wants to resubmit the form content. I need to avoid this.
This was my code handling the closing of the .vote-form:
$('.vote-form-close').click(function(event) {
event.stopPropagation();
$(".vote-form").fadeOut("normal");
$("#the-lights").fadeTo("slow",0);
$("#the-lights").css({'display' : 'none'});
window.location.reload();
});
I suspect that its possible to refresh ONLY the div, and not the entire page, but I do not know how to accomplish it.
Can someone assist me?
EDIT: Based on one of the answers below, I modified my code. I also wanted to show the code used to open the form up too:
$('.vote').click(function() {
$(this).parent().find(".vote-form").fadeIn("normal");
$("#the-lights").css({'display' : 'block'});
$("#the-lights").fadeTo("slow",0.7);
});
$('.vote-form-close').click(function(event) {
event.stopPropagation();
$(".vote-form").fadeOut("normal");
$("#the-lights").fadeTo("slow",0);
$("#the-lights").css({'display' : 'none'});
$(".vote-form").load(window.location.href + " .vote-form-container");
});
Here is the problem - I have 3 forms on the page. When "vote-form-container" is loaded, its loading ALL THREE forms into the .vote-form box - how do I modify the code to only load the .vote-form-container that is part of the specific .vote-form - I suspect I have to use $(this) but I tried modifying the code to this and it didnt work:
$(".vote-form")(this).load(window.location.href + " .vote-form-container");
I am thinking I did it wrong.
EDIT 2: Now the "Close" button dosen't work after the form is reloaded the first time:
$('.vote').click(function() {
$(this).parent().find(".vote-form").fadeIn("normal");
$("#the-lights").css({'display' : 'block'});
$("#the-lights").fadeTo("slow",0.7);
});
$('.vote-form-close').click(function(event) {
event.stopPropagation();
$(".vote-form").fadeOut("normal");
$("#the-lights").fadeTo("slow",0);
$("#the-lights").css({'display' : 'none'});
var current_form = $(this).closest('.vote-form'),
index = $('.vote-form').index(current_form)
current_form.load(window.location.href + " .vote-form-container:eq(" + index + ")");
});
Don't reload the page but redirect your user:
window.location.href = window.location.href.toString()
Or load the new form with ajax:
$(".vote-form").load(window.location.href + " .vote-form");
For more information on the ajax approach see api.jquery.com/load/#loading-page-fragments
Update:
Using jQuery the index function you are able to replace only the current form.
// I asume your button is in the form
var current_form = $(this).closest('.vote-form'),
index = $('.vote-form').index(current_form)
current_form.load(window.location.href + " .vote-form:eq(" + index + ")");
... I need to make it so that when the jQuery popup is closed, the success message is removed and the form is refreshed back to the original form...
So, if I well understood:
$('.vote-form-close').click(function(event) {
event.stopPropagation();
var vf = $(".vote-form");
/* fadeout and remove inner content of the popup */
vf.fadeOut("normal", function() { vf.empty(); });
/* reset the form */
document.getElementById('form').reset();
...
});