Make code wait for popup to open, then scrape the popup - javascript

I am writing a JS script that automates some browser actions.
With get_baskets_onclicks, I am collecting onclick functions from certain DOM elements and returning them in an array. Each onclick looks something like this:
onclick="PrimeFaces.ab({s:"j_id_32:GenerationTable:0:j_id_1e_2_3p",u:"#widgetVar(GenerationCodingDialog)",onco:function(xhr,status,args){PF('GenerationCodingDialog').show();}});return false;"
and opens a pop-up from which I need to collect some data with get_MAP_data.
Also, each of these functions is called from within get_MAP_data.
The problem is I cannot make the code wait for the popup to be opened, so the data returned by get_MAP_data is empty.
Besides the below document.readyState === 'complete', I have also tried window.onload = function(){}, to no avail.
Is there any way to make the browser (Chrome) wait? I guess I cannot use jQuery, because this is not my webpage.
function get_baskets_onclicks() {
// returns array of functions that launch MAP dialogs
var baskets = Array.from(document.getElementsByClassName("ui-commandlink ui-widget margin-right-5px"));
var baskets_onclicks = baskets.map(basket => basket.onclick);
return baskets_onclicks;
};
function get_MAP_data(basket_onclick) {
basket_onclick()
if (document.readyState === 'complete') {
console.log("PAGE LOADED");
// wait here for the dialog to open
// dt = detail table
var MAP_data = {} // container for transaction details
var labels_to_get = ['Description', 'Category', 'Department', 'Justification', 'My Shop Voucher', 'My Shop Coding'];
var all_dts = document.getElementsByClassName('summary-details-grid');
var dt = Array.from(all_dts).filter(table => table.parentElement.id == "paymentGenerationmyShopCodingForm")[0];
var dt_body = dt.children[0];
var dt_trs = Array.from(dt_body.children) ;
dt_trs.forEach(function(tr) {
tds = Array.from(tr.children);
tds.forEach(function(td) {
var label = td.textContent;
if (labels_to_get.includes(label)) {
var value_for_label = tds[1].textContent;
MAP_data[label] = value_for_label;
console.log(label, value_for_label);
};
});
});
// console.log(MAP_data);
return MAP_data;
};
};
var first_onclick = get_baskets_onclicks()[0];
get_MAP_data(first_onclick);

A small, hacky fix would be to make your code poll for the existence of the elements you are checking.
let interval = setInterval(() => {
var all_dts = document.getElementsByClassName('summary-details-grid');
if (all_dts.length !== 0) {
// Found some elements, now lets run the code
clearInterval(interval);
get_MAP_data(first_onclick);
}
}, 100);
This would check for summary-details-grid classes ever 10th of a second, and when it finds them, run your code.

https://developer.mozilla.org/en-US/docs/Talk:DOM/window.setTimeout
http://mdn.beonex.com/en/DOM/window.setInterval.html
combination of retries, setTimeout, setInterval, while loop, I'm not sure the best way for your specific modal, but one of those options should be fit for polling the DOM results until something is there or it's tried to many times.

Related

Is there a way to call a specific javascript function in a single js file for a specific page URL?

I have a question, i wanted to know if there is a way to call a specific function if a specific page URL is opened?
#shop_modal is in http://myurl/liste-boutique.php
#dep_modal is in http://myurl/index.php
Right now, i have Error when i try to open the #dep_modal because JS cant find #shop_modal on that page same page, so it does not execute below that.
I think AJAX can help figuring this out, otherwise i will have to split the code in 2 JS files, which i don't want to
const new_shop = $("#new_shop")[0];
const save_shop = $("#shop_form")[0];
const close_shop_modal = $("#close_shop_modal")[0];
const new_departement = $("#new_dep")[0];
const save_departement = $("#dep_form")[0];
const close_dep_modal = $("#close_dep_modal")[0];
// I want this to be called if the URL is http://my-URL/liste-boutique.php
new_shop.addEventListener('click', function(){
$("#shop_modal")[0].style.visibility = "visible";
})
// I want this to be called if the URL is http://my-URL/index.php
new_departement.addEventListener('click', function(){
$("#dep_modal")[0].style.visibility = "visible";
})
i need to ask question, but i don't know what to change here
Thanks again !!
You can check window.location.href. E.g.
if (window.location.href === 'http://my-URL/liste-boutique.php') {
new_shop.addEventListener('click', function(){
$("#shop_modal")[0].style.visibility = "visible";
});
}
Instead of checking the url, check if the element you want to find is on the page:
var $new_shop = $("#new_shop");
if ($new_shop.length > 0) {
var new_shop = $new_shop[0];
new_shop.addEventListener('click', function(){
$("#shop_modal")[0].style.visibility = "visible";
})
}
(I've used $ prefix on $new_shop to show it's a jquery object just for clarity)
Or, using your code as-is:
var new_shop = $("#new_shop")[0];
if (new_shop != undefined) {
new_shop.addEventListener...
Alternatively, if you use jquery, you don't need to worry about it as it will automatically not apply if the element doesn't exist:
$("#new_shop").click(() => { $("#shop_modal)").fadeIn(); });

Chrome Extension: Adding event listener not working after site loads

I'm building a Chrome Extension to add some shortcut functionality to a site I regularly work with. I've tried calling my addTypingListeners() to bind the div with 2 inputs that I've added to the title and subtitle of the edit page I'm working on. However, I never seem to get into the document.eventListener closure.
My Chrome Extension is run at document_idle so the content should be loaded by the time my additional code runs. How can I get these listeners to embed on the page?
Even when I don't call addTypingListeners(), I still see a and b log in the console
function addTypingListeners() {
console.log('a')
var meta = {}
document.addEventListener("DOMContentLoaded",()=>{
console.log('listeners added pre')
bind(meta, document.getElementsByTagName('title'), "title");
bind(meta, document.getElementsByTagName('subtitle'), "subtitle");
setInterval(()=>{document.getElementsByTagName('h3')[0].innerText=meta.title});
setInterval(()=>{
console.log(meta)
document.getElementsByTagName('h4')[0].innerText = meta.subtitle
});
console.log('listeners added')
})
console.log('b')
}
const start = async function() {
// var location = window.location.toString()
let slug = window.location.toString().split("/")[4]
let url = `https://example.org/${slug}?as=json`
const _ = await fetch(url)
.then(res => res.text())
.then(text => {
let obj = JSON.parse(text);
const { payload } = obj;
// Container
const root = document.getElementById('container');
var clippyContainer = document.createElement('div');
createShell(clippyContainer, name);
root.appendChild(clippyContainer);
// Inputs
const title = document.getElementsByTagName('h3')[0];
const subtitle = document.getElementsByTagName('h4')[0];
var inputDiv = document.createElement('div');
inputDiv.id = "input-div";
const titleInput = document.createElement('input');
titleInput.id = "title"
titleInput.value = title.innerText;
inputDiv.appendChild(titleInput);
const breaker = document.createElement("br")
inputDiv.appendChild(breaker);
const subtitleInput = document.createElement('input');
subtitleInput.id = "subtitle"
subtitleInput.value = subtitle.innerText;
inputDiv.appendChild(subtitleInput);
clippyContainer.appendChild(inputDiv);
inputDiv.appendChild(breaker);
// addTypingListeners() // tried here, also doesn't work
});
}
start()
.then( (_) => {
console.log('hi')
addTypingListeners()
console.log("done")
})
Probably the event DOMContentLoaded was already fired at the point of time when you set the listener. You can check that document.readyState equals to complete and execute the function without subscribing to the event if it already occurred. In the opposite case if the readyState is loading or interactive you should set the listener as it is currently done in the attached example.
The code you provided should be injected to the page as a content script (for details).
According to the official documentation, the order of events while a page is loading:
document_start > DOMContentLoaded > document_end > load > document_idle.
The difference between load and DOMContentLoaded events is explained here as
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
Thus, you should add the listeners without waiting for the DOMContentLoaded event, which will never fire.
This is literally all the coded need besides whatever your doing to the dom.
Background.js
let slug = window.location.toString().split("/")[4]
let url = `https://example.org/${slug}?as=json`
fetch(url).then(res => res.text()).then((data) => {
chrome.tabs.sendMessage(tabId, {
message: data
});
})
Content.js
function addTypingListeners(data) {
// Update page dom
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message) {
addTypingListeners(request.message);
}
});

Each click triggers different API call

I have list and each clicked item triggers different API request. Each request have different duration. On success I'm displaying some data.
Issue is that when I click on item#1 which takes approx 6000 to load, and just after on item#2 which takes 2000 to load, I will have the last clicked item displayed - which is item#2 because it has already loaded and once item#1 has received data my data will change to that. This is wrong as I want to display data from the latest click.
This is how I handle event:
newList.on('click', 'li', (e) => {
let id = $(e.currentTarget).data("id");
store.getCharacterDetails(id).then(docs => {
this.clearDetails();
this.charDetails = docs;
this.displayDetails(this.charDetails);
})
My API is a simulation from store object.
I suppose this works as expected but I do want the last triggered request to be valid.
A crude and simple method can be creating an array and pushing the IDs and after the asynchronous operations you can just check if it is the latest click or not. But pitfall is that if clear and displayDetails takes much time and if someone click while it was clearing and displaying it will not register the latest click.
Anyway, here is the code maybe you can make something better out of it.
var latestClick = [];
newList.on('click', 'li', (e) => {
let id = $(e.currentTarget).data("id");
latestClick.push(id);
store.getCharacterDetails(id).then(docs => {
if(id === latestClick[latestClick.length - 1]){
this.clearDetails();
this.charDetails = docs;
this.displayDetails(this.charDetails);
latestClick = [];
}
})
})
Make charDetails an object that keeps all of the results, keyed by the ids. Keep track of the last clicked id.
// in constructor
this.charDetails = {};
this.lastId = null;
newList.on('click', 'li', (e) => {
let id = $(e.currentTarget).data("id");
this.lastId = id;
if (this.charDetails[id] === id) { // don't cancel requests, cache them!
this.displayDetails(this.charDetails[id])
} else {
store.getCharacterDetails(id).then(docs => {
// this runs later, cache the result
this.charDetails[id] = docs;
if (id === lastId) { // only update UI if the id was last clicked
this.displayDetails(docs)
}
});
}
});

.each() loop wait for another loop to finish before starting

I have two main loops for posts and comments. However the comments don't display, presumably because the post ID is not on the DOM yet (?).
$.getJSON('fresh_posts.php',function(data){
//posts
$.each(data.freshposts, function(id, post) {
// set variables and append divs to document
var id = post.id;
...
});
// comments attached to each post
$.each(data.freshcomments, function(id, commentList) {
$.each(commentList, function(index, c) {
// set variables and append comments to each post div
var postid = c.postid; // this is the same as post.id (linked)
...
var full = "<div> ... </div>";
$('#comment-block'+postid).append(full); // comment-block+postid is attached with each post div, so it tells the comment which div it should be appended to.
})
});
});
Does not display comments ^
If I wrap the $.each loop for the comments in a setTimeOut(function(){},1), the comments are able to be displayed - I suppose it needs to wait 1 millisecond before the loop can commence? However this doesn't seem like a good/fool-proof way to ensure this.
setTimeOut(function(){
$.each(data.freshcomments, function(id, commentList) {
...
})
},1)
Displays comments ^
I managed to solve this after a few hours of work.
I made two functions, getPosts() and appendComments(). I used the promise method:
function getPosts(){
var deferred = new $.Deferred(); // new deferred
$.getJSON('fresh_posts.php',function(data){
global_save_json = data.freshcomments;
var howManyPosts = Object.keys(data.freshposts).length; // how many posts there are (object length)
var arrayCount = 0;
$.each(data.freshposts, function(id, post) {
// closing tags for above
return deferred.promise();
}
I had an async method (just learned what that was) to check if the image was designed for retina displays within getPosts.
if (retina === "true") {
var img = new Image();
img.onload = function() {
var retina_width = this.width/2;
var post = '...';
$('.main').append(post);
arrayCount++; // for each post add +1 to arrayCount
if (arrayCount == howManyPosts) { // if last post
deferred.resolve();
}
}
img.src = image;
} else {
...
I then did
getPosts().then(appendComments);
after closing tag of the function getPosts. So basically even though the function finishes, it still waits for the async method to also finish within that function before the function getComments is called. That way the post id's will exist on the document for the comments to be appended to.
appendComments looks like this:
function appendComments(){
if (global_save_json != null){
$.each(global_save_json, function(id, commentList) {
$.each(commentList, function(index, c) {

Javascript, execute scripts code in order inserted into dom tree

NOT A DUPLICATE AS I HAVE YET TO FOUND A SATISFYING ANSWER ON OTHER THREADS:
Load and execute javascript code SYNCHRONOUSLY
Loading HTML and Script order execution
Load and execute javascript code SYNCHRONOUSLY
Looking for native Javascript answers, no jQuery, no requireJS, and so forth please :)
SUMMARY OF THE ENTIRE QUESTION:
I want to asynchronously load scripts but have ordered execution
I am trying to enforce that the code in the inserted script elements execute exactly in the same order as they were added to the dom tree.
That is, if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first.
I have tried with the async attribute and defer attribute when inserting into the head but doesn't seem to obey.
I have tried with element.setAttribute("defer", "") and element.setAttribute("async", false) and other combinations.
The issue I am experiencing currently has to do when including an external script, but that is also the only test I have performed where there is latency.
The second script, which is a local one is always fired before the first one, even though it is inserted afterwards in the dom tree ( head ).
A) Note that I am still trying to insert both script elements into the DOM. Ofcourse the above could be achieved by inserting first, let it finish and insert the second one, but I was hoping there would be another way because this might be slow.
My understanding is that RequireJS seems to be doing just this, so it should be possible. However, requireJS might be pulling it off by doing it as described in A).
Code if you would like to try directly in firebug, just copy and paste:
function loadScript(path, callback, errorCallback, options) {
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", path);
return loadElement(element, callback, errorCallback, options);
}
function loadElement(element, callback, errorCallback, options) {
element.setAttribute("defer", "");
// element.setAttribute("async", "false");
element.loaded = false;
if (element.readyState){ // IE
element.onreadystatechange = function(){
if (element.readyState == "loaded" || element.readyState == "complete"){
element.onreadystatechange = null;
loadElementOnLoad(element, callback);
}
};
} else { // Others
element.onload = function() {
loadElementOnLoad(element, callback);
};
}
element.onerror = function() {
errorCallback && errorCallback(element);
};
(document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);
return element;
}
function loadElementOnLoad(element, callback) {
if (element.loaded != true) {
element.loaded = true;
if ( callback ) callback(element);
}
}
loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
alert(1);
})
loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
})
If you try the above code in like firebug, most often it will fire 2, and then 1. I want to ensure 1 and then 2 but include both in the head.
if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first. I have tried with the async attribute and defer attribute
No, async and defer won't help you here. Whenever you dynamically insert script elements into the DOM, they are loaded and executed asynchronically. You can't do anything against that.
My understanding is that RequireJS seems to be doing just this
No. Even with RequireJS the scripts are executed asynchronous and not in order. Only the module initialiser functions in those scripts are just define()d, not executed. Requirejs then does look when their dependencies are met and executes them later when the other modules are loaded.
Of course you can reinvent the wheel, but you will have to go with a requirejs-like structure.
Ok, I think I have now came up with a solution.
The trick is that we keep track of each script to be loaded and their order as we insert them into the dom tree. Each of their callback is then registered accordingly to their element.
Then we keep track of when all has finished loading and when they all have, we go through the stack and fire their callbacks.
var stack = [];
stack.loaded = 0;
function loadScriptNew(path, callback) {
var o = { callback: callback };
stack.push(o);
loadScript(path, function() {
o.callbackArgs = arguments;
stack.loaded++;
executeWhenReady();
});
}
function executeWhenReady() {
if ( stack.length == stack.loaded ) {
while(stack.length) {
var o = stack.pop();
o.callback.apply(undefined, o.callbackArgs);
}
stack.loaded = 0;
}
}
// The above is what has been added to the code in the question.
function loadScript(path, callback) {
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", path);
return loadElement(element, callback);
}
function loadElement(element, callback) {
element.setAttribute("defer", "");
// element.setAttribute("async", "false");
element.loaded = false;
if (element.readyState){ // IE
element.onreadystatechange = function(){
if (element.readyState == "loaded" || element.readyState == "complete"){
element.onreadystatechange = null;
loadElementOnLoad(element, callback);
}
};
} else { // Others
element.onload = function() {
loadElementOnLoad(element, callback);
};
}
(document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);
return element;
}
function loadElementOnLoad(element, callback) {
if (element.loaded != true) {
element.loaded = true;
if ( callback ) callback(element);
}
}
loadScriptNew("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.js",function() {
alert(1);
});
loadScriptNew("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
});
Ok, some of you might argue that there is missing info in the question, which I will give you and here we are actually just solving the callback. You are right. The code in the script is still executed in the wrong order, but the callback is now.
But for me this is good enough, as I intend to wrap all code that is loaded in a method call, alá AMD, such as a require or define call and will put on stack there, and then fire them in the callback instead.
I am still hoping out for Asad and his iframe solution, which I believe might provide the best answer to this question. For me though, this solution will solve my problems :)
I am posting here just like a draft
This do not work because cross-domain police
Here the idea is to obtain all scripts first and when they are in memory, execute them in order.
function loadScript(order, path) {
var xhr = new XMLHttpRequest();
xhr.open("GET",path,true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if(xhr.status >= 200 && xhr.status < 300 || xhr == 304){
loadedScripts[order] = xhr.responseText;
}
else {
//deal with error
loadedScripts[order] = 'alert("this is a failure to load script '+order+'");';
// or loadedScripts[order] = ''; // this smoothly fails
}
alert(order+' - '+xhr.status+' > '+xhr.responseText); // this is to show the completion order. Careful, FF stacks aletrs so you see in reverse.
// am I the last one ???
executeAllScripts();
}
};
}
function executeAllScripts(){
if(loadedScripts.length!=scriptsToLoad.length) return;
for(var a=0; a<loadedScripts.length; a++) eval(loadedScripts[a]);
scriptsToLoad = [];
}
var loadedScripts = [];
var scriptsToLoad = [
"http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",
"http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",
"http://nowhere.existing.real_script.com.ar/return404.js"
];
// load all even in reverse order ... or randomly
for(var a=0; a<scriptsToLoad.length; a++) loadScript(a, scriptsToLoad[a]);
After a while of fiddling around with it, here is what I came up with. Requests for the scripts are sent off immediately, but they are executed only in a specified order.
The algorithm:
The algorithm is to maintain a tree (I didn't have time to implement this: right now it is just the degenerate case of a list) of scripts that need to be executed. Requests for all of these are dispatched nearly simultaneously. Every time a script is loaded, two things happen: 1) the script is added to a flat list of loaded scripts, and 2) going down from the root node, as many scripts in each branch that are loaded but have not been executed are executed.
The cool thing about this is that not all scripts need to be loaded in order for execution to begin.
The implementation:
For demonstration purposes, I am iterating backward over the scriptsToExecute array, so that the request for CFInstall is sent off before the request for angularJS. This does not necessarily mean CFInstall will load before angularJS, but there is a better chance of it happening. Regardless of this, angularJS will always be evaluated before CFInstall.
Note that I've used jQuery to make my life easier as far as creating the iframe element and assigning the load handler is concerned, but you can write this without jQuery:
// The array of scripts to load and execute
var scriptsToExecute = [
"http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js?t=" + Date.now(),
"http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js?t=" + Date.now()
];
// Loaded scripts are stored here
var loadedScripts = {};
// For demonstration purposes, the requests are sent in reverse order.
// They will still be executed in the order specified in the array.
(function start() {
for (var i = scriptsToExecute.length - 1; i >= 0; i--) {
(function () {
var addr = scriptsToExecute[i];
requestData(addr, function () {
console.log("loaded " + addr);
});
})();
}
})();
// This function executes as many scripts as it currently can, by
// inserting script tags with the corresponding src attribute. The
// scripts aren't reloaded, since they are in the cache. You could
// alternatively eval `script.code`
function executeScript(script) {
loadedScripts[script.URL] = script.code
while (loadedScripts.hasOwnProperty(scriptsToExecute[0])) {
var scriptToRun = scriptsToExecute.shift()
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", scriptToRun);
$('head').append(element);
console.log("executed " + scriptToRun);
}
}
// This function fires off a request for a script
function requestData(path, loadCallback) {
var iframe = $("<iframe/>").load(function () {
loadCallback();
executeScript({
URL: $(this).attr("src"),
code: $(this).html()
});
}).attr({"src" : path, "display" : "none"}).appendTo($('body'));
}
You can see a demo here. Observe the console.
cant you nest the loading using ur callbacks?
ie:
loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
alert(1);
loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
})
})

Categories