Clicking on anchor tag will only excute part of a function - javascript

I am currently facing an issue where I have event listeners running for my client's website which track all hover and clicks on the website. We have 4 clients and our code works fine on 3 of them but it does not work correctly on the 1 client's website.
We are running the following code when a click is triggered:
async function objectClick(obj) {
var tag = obj.target;
if (tag.nodeName == 'IMG') {
objectName = tag.src;
} else {
objectName = tag.innerText || tag.textContent || tag.value;
}
var data_tag = tag.getAttribute("data-*");
if (data_tag != null) {
if (data_tag.includes("abc")) {
var layout = JSON.parse(window.localStorage.getItem(page));
layout.ctr = 1;
window.localStorage.setItem(page, JSON.stringify(layout))
await sendToHistory(uid, url, JSON.stringify(layout));
} else if (data_tag.includes("reward")) {
var layout = JSON.parse(window.localStorage.getItem(page));
layout.reward = 1;
window.localStorage.setItem(page, JSON.stringify(layout))
await sendToHistory(uid, url, JSON.stringify(layout));
}
}
if (data_tag === "abc" || data_tag === "reward") {
await sendToJourney(uid, "Clicked", -1, tag.nodeName, JSON.stringify({ text: objectName, data_nostra: null }), url, page);
} else {
await sendToJourney(uid, "Clicked", -1, tag.nodeName, JSON.stringify({ text: objectName, data_nostra: data_nostra_tag }), url, page);
}
}
For most of our clients, all the code runs in the function runs, including the sendToJourney function. For this client, after sendToHistory runs, the page switches and sendToJourney is not triggered. Do you know why this is?
sendToJourney and sendToHistory are functions that send some data to an API. Let me know if you need more information. Lastly, the client's website is created using Elementor which is a WordPress plugin. 2 of the other 4 clients also use Elementor but the function is fully executed for them but just not for this 1 client. Is there something that can be preventing the code from fully executing?
We have tried using obj.preventDefault but we cannot get the event to be triggered afterwards once our code is executed so what solution could be use here?
I call objectClick() by attaching an event listener as such:
(async function (document) {
await initData()
var trackable = document.querySelectorAll("img,button,p,h1,h2,h3,h4,h5,h6,a,span,input[type='submit'],[data-*]");
for (var i = 0; i < trackable.length; i++) {
trackable[i].addEventListener('mouseover', onHover, false);
trackable[i].addEventListener('mouseout', offHover, false);
trackable[i].addEventListener('click', objectClick, false);
}
})(document);

Related

MutationObserver callback not called in Firefox but in Chrome

I have a problem:
Every resource I looked for tells me this should work and it does in Chrome, Safari.. but not in Firefox.
System: macOS Catalina 10.15.7, Chrome 90.0.4430.212, Firefox 88.0.1, Edge 90.0.818.66
What I try to achieve / have achieved on Chrome: finding an iFrame, that is hosted on the same domain, and targeting some child nodes there and changing attributes, removing text inputs etc.
A simplified version of my JS code:
// set as null first because the iFrame takes some time to load
let iframeBody = null;
// this function is only run once, while it should be run on every MutationObserver change
function purge (iframe) {
var timer = null;
// only by querying that iFrame again Firefox picked up the purge function, I don't need that for Chrome
let iF = document.querySelector('#chatbot iframe').contentWindow.document.body;
var inputField = iF.querySelector('div > div.frame-content > div.widget-position-right > div.chat > div.input-group');
if(!inputField || typeof inputField === 'undefined') {
if(timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(purge, 50);
return;
}
// remove it
inputField.remove();
}
// this one is never called in FF
function cb (mutationList) {
let chatDiv = iframeBody.querySelector('div > div.frame-content > div.widget-position-right > div.chat')
if (chatDiv) {
mutationList.some(item => {
if (item.type === 'childList' && item.addedNodes && item.addedNodes[0] === chatDiv) {
purge();
return true;
}
})
}
let button = iframeBody.querySelector("div > div.frame-content > div.widget-position-right > #button")
// make button invisible if found, restore if chat is closed
if (button) {
if (button.classList.contains('chat-open')) {
button.style.opacity = 0;
button.style.pointerEvents = "none";
button.style.width = 0;
}
else {
button.style.opacity = 1;
button.style.pointerEvents = "auto";
button.style.width = "auto";
}
}
}
function bind () {
try {
iframeBody = document.querySelector('#chatbot iframe').contentWindow.document.body;
if (!iframeBody) {
return setTimeout(bind, 500) // wait and retry because iFrame is not loaded immediately
}
if(iframeBody){
console.log("iframeBody found");
}
const mutationObservable = new MutationObserver(cb)
// actually don't need characterData, just an attempt to pick up the observed event
const config = { characterData: true, attributes: true, childList: true, subtree: true};
setTimeout(() => {
// this is the initial call that works in FF (and Chrome/Safari naturally)
purge();
mutationObservable.observe(iframeBody, config);
}, 500);
} catch (err) {
// console.log(err);
// try to bind again, can take some time to load
setTimeout(bind, 100)
}
}
// start here
bind();
})();
I verified that I can access the HTML nodes with querySelector in the Developer console and edit them just fine in all browsers, it is just the MutationObserver callback that is not picked up.
I tried to put write it like this
new MutationObserver((mutation) => {
// cb function goes here
})
but to no avail. I have been sitting at this for a few hours now, just to get it to work, and I am tempted to just deactivate it all together for Firefox..
Any hints are very appreciated. If any additional resources are needed, please let me know.
Update: tried suggestion from comments with new frameElem.contentWindow.MutationObserver by calling MutationObserver like this without any change:
const iframeElmWindow = document.querySelector('#chatbot iframe').contentWindow;
const mutationObservable = new iframeElmWindow.MutationObserver(cb);
The problem is certainly that you caught the initial about:blank's Document's <body> in your iframeBody variable.
Here is an outsourced1 minimal example that shows that your code may catch that initial Document and not the one that gets loaded later on.
// removed a lot of unrelated stuff from OP,
// to keep only the core of the issue
function bind() {
const iframeBody = frame.contentWindow.document.body;
if( !iframeBody ) {
return setTimeout( bind, 500 );
}
if( iframeBody ) {
console.log( "iframeBody found" );
console.log( "location:", iframeBody.ownerDocument.location.href );
// logs "about:blank"
}
bind();
So the one <body> you are observing is the one of that temporary initial about:blank Document. And since this Document will get replaced in a few ms by the loaded Document, no new Mutation will ever happen there.
The easiest to workaround that is to use the <iframe>'s load event, which will fire only when the loaded Document actually has loaded.
This way, you are sure you won't catch the initial about:blank Document, and you don't need your setTimeout( bind ) hack anymore: if ever you can't access the iframe's contentDocument in this event, it's because both context are not same-origin, and trying again won't fix it.
So all in all, to start a MutationObserver targeting your <iframe>'s body you'd do:
frame.onload = function( evt ) {
const iframeBody = frame.contentDocument?.body;
if( !iframeBody ) {
// this means we are in a cross-origin document...
return;
}
const observer = new MutationObserver( cb );
observer.observe( iframeBody, { childList: true, subtree: true } );
};
Once again as an outsourced example1.
1. We need to outsource these examples because StackSnippet's null-origined iframes won't allow our script to access the content of an iframe, no matter how it's being served.

Don't show page until content has fully loaded

I am creating a landing page which should exist in two languages. The texts that should be shown are in two JSON files, called accordingly "ru.json" and "en.json". When a user clicks on the "Change language" button, the following function is executed:
function changeLang(){
if (userLang == 'ru') {
userLang = 'en';
document.cookie = 'language=en';
}
else {
userLang = 'ru';
document.cookie = 'language=ru';
}
var translate = new Translate();
var attributeName = 'data-tag';
translate.init(attributeName, userLang);
translate.process();
}
Where Translate() is the following:
function Translate() {
//initialization
this.init = function(attribute, lng){
this.attribute = attribute;
if (lng !== 'en' && lng !== 'ru') {
this.lng = 'en'
}
else {
this.lng = lng;
}
};
//translate
this.process = function(){
_self = this;
var xrhFile = new XMLHttpRequest();
//load content data
xrhFile.open("GET", "./resources/js/"+this.lng+".json", false);
xrhFile.onreadystatechange = function ()
{
if(xrhFile.readyState === 4)
{
if(xrhFile.status === 200 || xrhFile.status == 0)
{
var LngObject = JSON.parse(xrhFile.responseText);
var allDom = document.getElementsByTagName("*");
for(var i =0; i < allDom.length; i++){
var elem = allDom[i];
var key = elem.getAttribute(_self.attribute);
if(key != null) {
elem.innerHTML = LngObject[key] ;
}
}
}
}
};
xrhFile.send();
}
Everything works fine, however, when a user opens the page for the first time, if his Internet connection is bad, he just sees the elements of the page without text. It is just 1-2 seconds, but still annoying.
The question is, is there any way to check the text has loaded and display the page elements only on this condition?
You can use $(document).ready() in this way
$(document).ready(function(){
//your code here;
})
You can use the JavaScript pure load event in this way
window.addEventListener('load', function () {
//your code right here;
}, false);
Source: Here
translate.process() is asynchronous code which needs to make a call to a server and wait for its response. What it means is that, when you call this function, it goes in the background to go do its own thing while the rest of the page continues loading. That is why the user sees the page while this function is still running.
One minimal way I can think around this is by adding this to your css files in the head tag.
body { display: none }
And then, under this.process function, after the for loop ends, add
document.body.style.display = 'block'
If you want to suppori IE8:
document.onreadystatechange = function () {
if (document.readyState == "interactive") {
// run some code.
}
}
Put the code you want to execute when the user initially loads the page in a DOMContentLoaded event handler like below:
document.addEventListener('DOMContentLoaded', function() {
console.log('Whereas code execution in here will be deffered until the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.');
});
console.log('This will log immediatley');
It's important to note that DOMContentLoaded is different than the load event

infinite scroll on squarespace get category filter

I am using this code to infinite load a page on squarespace. My problem is the reloading doesn't capture the filtering that I have set up in my url. It cannot seem to 'see' the variables or even the url or categoryFilter in my collection. I've tried to use a .var directive but the lazy loaded items cannot see the scope of things defined before it. I'm running out of ideas here please help!
edit: I've since found the answer but gained another question.
I was able to use window.location.href instead of window.location.pathname to eventually get the parameters that way. Except this doesn't work in IE11 so now I have to search for this.
<script>
function infiniteScroll(parent, post) {
// Set some variables. We'll use all these later.
var postIndex = 1,
execute = true,
stuffBottom = Y.one(parent).get('clientHeight') + Y.one(parent).getY(),
urlQuery = window.location.pathname,
postNumber = Static.SQUARESPACE_CONTEXT.collection.itemCount,
presentNumber = Y.all(post).size();
Y.on('scroll', function() {
if (presentNumber >= postNumber && execute === true) {
Y.one(parent).append('<h1>There are no more posts.</h1>')
execute = false;
} else {
// A few more variables.
var spaceHeight = document.documentElement.clientHeight + window.scrollY,
next = false;
/*
This if statement measures if the distance from
the top of the page to the bottom of the content
is less than the scrollY position. If it is,
it's sets next to true.
*/
if (stuffBottom < spaceHeight && execute === true) {
next = true;
}
if (next === true) {
/*
Immediately set execute back to false.
This prevents the scroll listener from
firing too often.
*/
execute = false;
// Increment the post index.
postIndex++;
// Make the Ajax request.
Y.io(urlQuery + '?page=' + postIndex, {
on: {
success: function (x, o) {
try {
d = Y.DOM.create(o.responseText);
} catch (e) {
console.log("JSON Parse failed!");
return;
}
// Append the contents of the next page to this page.
Y.one(parent).append(Y.Selector.query(parent, d, true).innerHTML);
// Reset some variables.
stuffBottom = Y.one(parent).get('clientHeight') + Y.one(parent).getY();
presentNumber = Y.all(post).size();
execute = true;
}
}
});
}
}
});
}
// Call the function on domready.
Y.use('node', function() {
Y.on('domready', function() {
infiniteScroll('#content','.lazy-post');
});
});
</script>
I was able to get this script working the way I wanted.
I thought I could use:
Static.SQUARESPACE_CONTEXT.collection.itemCount
to get {collection.categoryFilter} like with jsont, like this:
Static.SQUARESPACE_CONTEXT.collection.categoryFilter
or this:
Static.SQUARESPACE_CONTEXT.categoryFilter
It didn't work so I instead changed
urlQuery = window.location.pathname
to
urlQuery = window.location.href
which gave me the parameters I needed.
The IE11 problem I encountered was this script uses
window.scrollY
I changed it to the ie11 compatible
Window.pageYOffset
and we were good to go!

Service Worker onClick event - How to open url in window withing sw scope?

I have service worker which handles push notification click event:
self.addEventListener('notificationclick', function (e) {
e.notification.close();
e.waitUntil(
clients.openWindow(e.notification.data.url)
);
});
When notification comes it takes url from data and displays it in new window.
The code works, however, I want different behavior. When User clicks on the link, then it should check if there is any opened window within service worker scope. If yes, then it should focus on the window and navigate to the given url.
I have checked this answer but it is not exactly what I want.
Any idea how it can be done?
P.S. I wrote this code but it still doesn't work. The first two messages are however shown in the log.
self.addEventListener('notificationclick', function (e) {
e.notification.close();
var redirectUrl = e.notification.data.redirect_url.toString();
var scopeUrl = e.notification.data.scope_url.toString();
console.log(redirectUrl);
console.log(scopeUrl);
e.waitUntil(
clients.matchAll({type: 'window'}).then(function(clients) {
for (i = 0; i < clients.length; i++) {
console.log(clients[i].url);
if (clients[i].url.toString().indexOf(scopeUrl) !== -1) {
// Scope url is the part of main url
clients[i].navigate(givenUrl);
clients[i].focus();
break;
}
}
})
);
});
Ok, here is the piece of code which works as expected. Notice that I am passing scope_url together with redirect_url into the web notification. After that I am checking if scope_url is part of sw location. Only after that I navigate to redirect_url.
self.addEventListener('notificationclick', function (e) {
e.notification.close();
var redirectUrl = e.notification.data.redirect_url;
var scopeUrl = e.notification.data.scope_url;
e.waitUntil(
clients.matchAll({includeUncontrolled: true, type: 'window'}).then(function(clients) {
for (i = 0; i < clients.length; i++) {
if (clients[i].url.indexOf(scopeUrl) !== -1) {
// Scope url is the part of main url
clients[i].navigate(redirectUrl);
clients[i].focus();
break;
}
}
})
);
});
If I understand you correctly, most of the code you linked to works here.
First retrieve all the clients
If there are more than one, choose one of them
Navigate that to somewhere and focus
Else open a new window
Right?
event.waitUntil(
clients.matchAll({type: 'window'})
.then(clients => {
// clients is an array with all the clients
if (clients.length > 0) {
// if you have multiple clients, decide
// choose one of the clients here
const someClient = clients[..someindex..]
return someClient.navigate(navigationUrl)
.then(client => client.focus());
} else {
// if you don't have any clients
return clients.openWindow(navigationUrl);
}
})
);

javascript html5 history, variable initialization and popState

main question
Is there a javascript way to identify if we are accessing a page for the first time or it is a cause of a back?
My problem
I'm implementing html5 navigation in my ajax driven webpage.
On the main script, I initialize a variable with some values.
<script>
var awnsers=[];
process(awnsers);
<script>
Process(awnsers) will update the view according to the given awnsers, using ajax.
In the funciton that calls ajax, and replaces the view, I store the history
history.pushState(state, "", "");
I defined the popstate also, where I restore the view according to the back. Moreover, I modify the global variable awnsers for the old value.
function popState(event) {
if (event.state) {
state = event.state;
awnsers=state.awnsers;
updateView(state.view);
}
}
Navigation (back and forth) goes corectly except when I go to an external page, and press back (arrving to my page again).
As we are accessing the page, first, the main script is called,the valiable awnsers is updated, and the ajax starts. Meanwile, the pop state event is called, and updates the view. After that the main ajax ends, and updates the view according to empty values.
So I need the code:
<script>
var awnsers=[];
process(awnsers);
<script>
only be called when the user enters the page but NOT when it is a back. Any way to do this?
THanks!
Possible solution
After the first awnser I have thought of a possible solution. Tested and works, whoever, I don't know if there is any cleaner solution. I add the changes that I've done.
First I add:
$(function() {
justLoaded=true;
});
then I modify the popState function, so that is in charge to initialize the variables
function popState(event) {
if (event.state) {
state = event.state;
awnsers=state.awnsers;
updateView(state.view);
} else if(justLoaded){
awnsers=[];
process(awnsers);
}
justLoaded=false;
}
Thats all.
what about using a global variable?
var hasLoaded = false;
// this function can be called by dom ready or window load
function onPageLoad() {
hasLoaded = true;
}
// this function is called when you user presses browser back button and they are still on your page
function onBack() {
if (hasLoaded) {
// came by back button and page was loaded
}
else {
// page wasn't loaded. this is first visit of the page
}
}
Use cookie to store the current state.
yeah! This is what I have:
var popped = (($.browser.msie && parseInt($.browser.version, 10) < 9) ? 'state' in window.history : window.history.hasOwnProperty('state')), initialURL = location.href;
$(window).on('popstate', function (event) {
var initialPop = !popped && location.href === initialURL, state;
popped = true;
if (initialPop) { return; }
state = event.originalEvent.state;
if (state && state.reset) {
if (history.state === state) {
$.ajax({url: state.loc,
success: function (response) {
$(".fragment").fadeOut(100, function () {
$(".fragment").html($(".fragment", response).html()).fadeIn(100);
);
document.title = response.match(/<title>(.*)<\/title>/)[1];
}
});
} else { history.go(0); }
else {window.location = window.location.href; }
});
And:
$.ajax({url:link,
success: function (response) {
var replace = args.replace.split(",");
$.each(replace, function (i) {
replace[i] += ($(replace[i]).find("#video-content").length > 0) ? " #video-content" : "";
var selector = ".fragment "+replace[i];
$(selector).fadeOut(100, function () {
$(selector).html($(selector,response).html()).fadeIn(100, function () {
if (base.children("span[data-video]")[0]) {
if ($.browser.msie && parseInt($.browser.version, 10) === 7) {
$("#theVideo").html("");
_.videoPlayer();
} else {
_.player.cueVideoById(base.children("span[data-video]").attr("data-video"));
}
}
});
});
});
document.title = response.match(/<title>(.*)<\/title>/)[1];
window.history.ready = true;
if (history && history.pushState) { history.pushState({reset:true, loc:link}, null, link); }
}
});

Categories

Resources