Some bookmarklets don't execute on iOS: How to debug? - javascript
Update 2: Apparently, bookmarklets as long as 2410 characters do work fully after all, which I tested with a script of the form javascript: var a=0; a++; a++; ... window.alert(a);. At 9424 characters, Chrome sync for the bookmarklet failed, but by copy/pasting the bookmark from an Email, I was still able to execute it. It remains unclear, why some scripts work when served as a script tag (see Update 1), but fail when executed from a javscript: URL directly.
Update 1: It looks like the same bookmarklet will work, by generating a <script/> tag pointing to the script hosted on dropbox. This only really leaves as explanation, that the browser engine on iOS has a hardcoded limit of less than 1024 characters for javascript: URLs.
Original Post
I am trying to figure out why some bookmarklets do nothing – not even trigger an error – on iOS, having no access to a Mac OS device.
For reference, consider the following two bookmarklets (formatted source below):
// DebugSimple.js
javascript:/* DebugSimple.js - 10:02:50 2019-12-23 */(function(){ document.body.innerHTML='';document.body.style.all='initial';function putString(string){document.body.innerText+=string;}const oldConsoleLog=console.log;console.log=function(...args){putString('(log) '+args.map(e=>e.toString()).join(' '));return oldConsoleLog(...args);};window.addEventListener('error',event=>{var msg='\n(err) ';msg+=!event ?'!!event = '+event :!event.error ?'!!event.error = '+event.error :!event.error.stack ?'!!event.error.stack = '+event.error.stack : event.error.stack;if(event&&event.error)for(const key in event.error){msg+='\nevent.error.'+key+'='+event.error[key];}putString(msg);});console.log("Log test.");setTimeout(backtraceTest,10);function backtraceTest(){backtraceTest2();}function backtraceTest2(){noSuchFunctionShouldExist();}})();undefined;
// DebugOverlay.js
javascript:/* DebugOverlay.js - 10:07:25 2019-12-23 */(function(){ const highestZIndex=Math.max(...Array.from(document.getElementsByTagName('*')).map(e=>window.getComputedStyle(e).zIndex).map(parseInt).filter(e=>!isNaN(e)));console.log(highestZIndex);function append(parent,html,callback=undefined){var e=document.createElement('DIV');parent.appendChild(e);e.outerHTML=html;e=parent.lastElementChild;if(callback)callback(e);return e;}const ifr=document.createElement('IFRAME');document.body.prepend(ifr);ifr.id='debugOverlay.js';ifr.style.all='initial';ifr.style.zIndex=highestZIndex+1;ifr.style.position='fixed';ifr.style.left='5%';ifr.style.right='5%';ifr.style.width='90%';ifr.style.height='90%';ifr.style.opacity='90%';ifr.style.backgroundColor='#ddd';ifr.style.border='1pt solid #888';ifr.contentDocument;ifr.contentDocument.open();ifr.contentDocument.close();const header=append(ifr.contentDocument.body,"<DIV id='header'/>",e=>{e.style.position='static';});const closeButton=append(header,"<A href='#' id='closeButton'>[CLOSE]</A>",e=>{e.addEventListener('click',()=>ifr.parentElement.removeChild(ifr));});const clearButton=append(header,"<A href='#' id='clearButton'>[CLEAR]</A>",e=>{e.addEventListener('click',()=>{output.innerHTML='';});});const output=append(ifr.contentDocument.body,"<DIV id='outputPane'/>",e=>{e.style.fontFamily='Sans';});append(ifr.contentDocument.body,"<DIV id='overScroll' style='height:30vh'/>");var fontSizeScale=0.8;function setOutputFontSize(scale){output.style.fontSize=(fontSizeScale*100)+'%';}setOutputFontSize(fontSizeScale);const fontSizePlus=append(header,"<A href='#' id='fontSizePlus'>[LGR]</A>",e=>{e.addEventListener('click',()=>{setOutputFontSize(fontSizeScale*=1.2);});});const fontSizeMinus=append(header,"<A href='#' id='fontSizePlus'>[SML]</A>",e=>{e.addEventListener('click',()=>{setOutputFontSize(fontSizeScale/=1.2);});});const toggleLog=append(header,"<A href='#' id='toggleLog'>[log(on)]</a>",e=>{var logVisibility=true;e.addEventListener('click',()=>{setLogVisibility(logVisibility=!logVisibility);});function setLogVisibility(bool){Array.from(output.getElementsByClassName('log')).forEach(ee=>{ee.style.display=bool ?'block':'none';e.innerText=bool ?'[log(on)]':'[log(off)]';});}setLogVisibility(logVisibility);});window.addEventListener('error',(evt)=>(addEntry(evt.error.stack)));function addEntry(string,category='error'){append(output,"<DIV class='entry "+category+"'/>",e=>{e.style.marginTop='0.5em';e.style.borderTop='1px solid #888';e.style.fontWeight=category=='error'?'bold':'';append(e,'<CODE/>',e=>{e.innerText=string;});});};const oldConsoleLog=console.log;console.log=function(...args){oldConsoleLog(...args);addEntry(args.join(' '),'log');};function testingIfErrorLogWorks_a(){testingIfErrorLogWorks_b();}function testingIfErrorLogWorks_b(){noSuchFunction();}setTimeout(testingIfErrorLogWorks_a,10);setTimeout(()=>console.log('LogTest'),20);setTimeout(testingIfErrorLogWorks_b,30);})();undefined;
I have added these as bookmarks on Google Chrome (Windows 10), and synchronized to iOS and Android.
On Android and Windows, both scripts work as expected:
DebugSimple.js replaces the body of the page by a log of errors with backtrace, and console.log invocations.
DebugOverlay.js ads an iframe overlay, where the errors are logged more cleanly.
On iOS, the simpler script DebugSimple.js will execute and log errors as expected, hence I know of that errors are being captured on iOS, albeit with less helpful output than on Android and Windows 10. This includes syntax errors, like typing javascript:}; into the URL bar (syntax error).
However, if I invoke the DebugOverlay.js script, even if I manually copy-paste it from the bookmarks, nothing happens at all; DebugSimple.js doesn't report any kind of error either.
I suspect, that it may be related to the length of the bookmarklet, as this behavior seems to occur for large bookmarklets mostly; But regardless, it seems strange that there is nothing at all, that would indicate the cause.
At this point I don't know how to proceed; Keep in mind that I don't have any Mac device.
Formatted source of DebugSimple.js
document.body.innerHTML = '';
document.body.style.all = 'initial';
function putString(string) {
document.body.innerText += string;
}
const oldConsoleLog = console.log;
console.log = function(...args) {
putString('(log) ' + args.map(e => e.toString()).join(' '));
return oldConsoleLog(...args);
};
window.addEventListener('error', event => {
var msg = '\n(err) ';
msg +=
!event ? '!!event = ' + event :
!event.error ? '!!event.error = ' + event.error :
!event.error.stack ? '!!event.error.stack = ' + event.error.stack :
event.error.stack;
if(event && event.error) for(const key in event.error) {
msg += '\nevent.error.' + key + '=' + event.error[key];
}
putString(msg);
});
// Test error.
console.log("Log test.");
setTimeout(backtraceTest,10);
function backtraceTest(){ backtraceTest2(); }
function backtraceTest2(){ noSuchFunctionShouldExist(); }
Formatted source of DebugOverlay.js
// DOES NOT WORK IN CHROME WHEN PASTED TO THE CONSOLE.
// The console messes somehow with error events.
// Should work when executed as bookmarklet or 'javascript:' URI.
const highestZIndex = Math.max(
...Array.from(document.getElementsByTagName('*'))
.map(e=>window.getComputedStyle(e).zIndex)
.map(parseInt)
.filter(e => !isNaN(e)));
console.log(highestZIndex);
function append(parent, html, callback = undefined) {
var e = document.createElement('DIV');
parent.appendChild(e);
e.outerHTML = html;
e = parent.lastElementChild;
if(callback) callback(e);
return e;
}
const ifr = document.createElement('IFRAME');
document.body.prepend(ifr);
ifr.id = 'debugOverlay.js';
ifr.style.all = 'initial';
ifr.style.zIndex = highestZIndex + 1;
ifr.style.position = 'fixed';
ifr.style.left = '5%';
ifr.style.right = '5%';
ifr.style.width = '90%';
ifr.style.height = '90%';
ifr.style.opacity = '90%';
ifr.style.backgroundColor = '#ddd';
ifr.style.border = '1pt solid #888';
// Firefox requires content to be initialized.
ifr.contentDocument;
ifr.contentDocument.open();
ifr.contentDocument.close();
const header = append(ifr.contentDocument.body, "<DIV id='header'/>", e => {
e.style.position = 'static';
});
const closeButton = append(header, "<A href='#' id='closeButton'>[CLOSE]</A>", e => {
e.addEventListener('click', () => ifr.parentElement.removeChild(ifr));
});
const clearButton = append(header, "<A href='#' id='clearButton'>[CLEAR]</A>", e => {
e.addEventListener('click', () => {
output.innerHTML = '';
});
});
const output = append(ifr.contentDocument.body, "<DIV id='outputPane'/>", e => {
e.style.fontFamily = 'Sans';
});
append(ifr.contentDocument.body, "<DIV id='overScroll' style='height:30vh'/>");
var fontSizeScale = 0.8;
function setOutputFontSize(scale) {
output.style.fontSize = (fontSizeScale * 100) + '%';
}
setOutputFontSize(fontSizeScale);
const fontSizePlus = append(header, "<A href='#' id='fontSizePlus'>[LGR]</A>", e => {
e.addEventListener('click', () => {
setOutputFontSize(fontSizeScale *= 1.2);
});
});
const fontSizeMinus = append(header, "<A href='#' id='fontSizePlus'>[SML]</A>", e => {
e.addEventListener('click', () => {
setOutputFontSize(fontSizeScale /= 1.2);
});
});
const toggleLog = append(header, "<A href='#' id='toggleLog'>[log(on)]</a>", e => {
var logVisibility = true;
e.addEventListener('click', () => {
setLogVisibility(logVisibility = !logVisibility);
});
function setLogVisibility(bool) {
Array.from(output.getElementsByClassName('log')).forEach(ee => {
ee.style.display = bool ? 'block' : 'none';
e.innerText = bool ? '[log(on)]' : '[log(off)]';
});
}
setLogVisibility(logVisibility);
});
window.addEventListener('error', (evt) => (addEntry(evt.error.stack)));
function addEntry(string, category='error') {
append(output, "<DIV class='entry "+category+"'/>", e => {
e.style.marginTop = '0.5em';
e.style.borderTop = '1px solid #888';
e.style.fontWeight = category == 'error' ? 'bold' : '';
append(e, '<CODE/>', e => { e.innerText = string; });
});
};
const oldConsoleLog = console.log;
console.log = function (...args) {
oldConsoleLog(...args);
addEntry(args.join(' '), 'log');
};
// For testing.
function testingIfErrorLogWorks_a() { testingIfErrorLogWorks_b(); }
function testingIfErrorLogWorks_b() { noSuchFunction(); }
setTimeout(testingIfErrorLogWorks_a,10);
setTimeout(()=>console.log('LogTest'), 20);
setTimeout(testingIfErrorLogWorks_b,30);
Related
Office.js outlook add-in issue
I'm trying to get the Body in Outlook and then update/set it with categories. My issue is this - when I debug it - it works fine. But when I don't debug from function to function - it gets all the way to the last function and just stops - updateBody(). What's really strang is if I remove the breakpoints on each function and just set a breakpoint on last function - never gets hit, but console will write out "Starting update body". All the console.logs are writing out data as expected. Not sure what is going on. Appreciate any help! Thanks. "use strict"; var item; var response; var tags; var updatedBody; Office.initialize = function () { $(document).ready(function () { // The document is ready item = Office.context.mailbox.item; debugger; getBodyType(); }); } function getBodyType() { item.body.getTypeAsync( function (resultBody) { if (resultBody.status == Office.AsyncResultStatus.Failed) { write(resultBody.error.message); } else { response = resultBody; console.log('Successfully got BodyType'); console.log(response.value); getCategories(); } }); } function getCategories() { tags = ""; // Successfully got the type of item body. // Set data of the appropriate type in body. item.categories.getAsync(function (asyncResult) { if (asyncResult.status === Office.AsyncResultStatus.Failed) { console.log("Action failed with error: " + asyncResult.error.message); } else { var categories = asyncResult.value; console.log("Categories:"); categories.forEach(function (item) { var tag = item.displayName; tags += '#' + tag.replace(/\s/g, "") + ' '; }); console.log('Successfully got tags'); console.log(tags); getBody(); } }); } function getBody() { var body = ""; updatedBody = ""; console.log("Starting get body"); if (response.value == Office.MailboxEnums.BodyType.Html) { item.body.getAsync( Office.CoercionType.Html, { asyncContext: "This is passed to the callback" }, function (result) { //Replace all the # tags and update again. body = result.value.replaceAll(/#(\w)+/g, "").trimEnd(); var domParser = new DOMParser(); var parsedHtml = domParser.parseFromString(body, "text/html"); $("body", parsedHtml).append("<div>" + tags + "</div>"); var changedString = (new XMLSerializer()).serializeToString(parsedHtml); if (changedString != "") { updatedBody = changedString; } console.log(updatedBody); updateBody(); }); } } function updateBody() { console.log("Starting update body"); item.body.setAsync( updatedBody, { coercionType: Office.CoercionType.Html }, function (result2) { console.log("Body updated"); }); } Image - With breakpoints on each function - works as expected Image - Without breakpoints - gets to updateBody() function. But the string updatedBody isn't logged. It somehow skips over that even though it's called before updateBody() on getBody() Image - Same code run via Script Lab - works just fine as well.
How can I run javascript file after dom manipulation made in another js file?
When I run my rails application and enter likeButton into the console it gives me Uncaught ReferenceError: likeButton is not defined at :1:1 (anonymous) # VM1591:1 I tried moving the script in html to head and body. I am currently trying to use DOMContentLoaded but it seems I'm missing something. My overall goal is to change the color of the button once pressed and also keep the color after page refresh. I am using sessionStorage for this process. I just want to make sure that likeButton variable is declared after html is loaded. If its possible to done in javascript only. //first js file const BASE_URL = "http://localhost:3000" const GPUS_URL = `${BASE_URL}/gpus` const USERS_URL = `${BASE_URL}/users` const gpuCollection = document.querySelector('#gpu-collection') let wish = sessionStorage.getItem('wish'); class Gpu { constructor(gpuAttributes) { this.title = gpuAttributes.title; this.price = gpuAttributes.price; this.features = gpuAttributes.features; this.link = gpuAttributes.link; this.image = gpuAttributes.image; this.id = gpuAttributes.id; } render() { let div = document.createElement('div'); div.classList.add('card'); let h = document.createElement('h2'); let t = document.createTextNode(`${this.title} ($${this.price})`); h.appendChild(t); div.appendChild(h); let h1 = document.createElement('h1'); h1.classList.add('gpu-cat'); h1.innerHTML = `${this.features}`; div.appendChild(h1); let button = document.createElement('button'); button.classList.add('list_btn'); button.innerHTML = '♡'; div.appendChild(button); let a = document.createElement('a'); let img = document.createElement('img'); a.href = `${this.link}`; a.target = '_blank'; img.src = `${this.image}`; img.classList.add('gpu-image'); a.appendChild(img); div.appendChild(a); gpuCollection.appendChild(div); } } //second js file document.addEventListener("DOMContentLoaded", function (){ let likeButton; SignUp(); logInUser(); logOutUser(); function putGpusOnDom(gpuArray){ gpuArray.forEach(gpu => { let newGpu = new Gpu(gpu) newGpu.render() }); likeButton = document.querySelector("button"); } function fetchGpus(){ fetch(GPUS_URL) .then(res => res.json()) .then(gpus => putGpusOnDom(gpus)) } const enableWish = () => { console.log(likeButton) sessionStorage.setItem('wish', 'red') } gpuCollection.addEventListener('click', function (){ wish = sessionStorage.getItem('wish'); if(wish !== 'red'){ enableWish(); }else{ disableWish(); } }); }) //html file ... <body> <div id = "gpu-collection"></div> <script type="text/javascript" src="src/Gpu.js"></script> <script type="text/javascript" src="src/index.js" ></script> </body> </html>
As I mentioned in a comment the like button is not available on DOMContentLoaded if it is added dynamically. You need to wait until the button has been placed in the DOM Use something like the following, I'm making some guesses here as there are some gaps in your code document.addEventListener("DOMContentLoaded", function (){ //document.querySelector("button"); not yet available //NOTE: The likeButton variable will ONLY be in scope INSIDE the event listener function // You will not be able to access directly in the console. let likeButton; SignUp(); logInUser(); logOutUser(); function putGpusOnDom(gpuArray){ gpuArray.forEach(gpu => { let newGpu = new Gpu(gpu) newGpu.render() }); //Now you have rendered the button it is available //CAUTION: querySelector("button") will grab the first button on the page // and ONLY the first button likeButton = document.querySelector("button"); //Log like button to console while it is still in scope. console.log(likeButton); } function fetchGpus(){ fetch(GPUS_URL) .then(res => res.json()) .then(gpus => putGpusOnDom(gpus)) } const enableWish = () => { console.log(likeButton) sessionStorage.setItem('wish', 'red') } })
scrapeAndClick function in APIFY
I have a following trouble in APIFY. I would like to write a function that saves HTML body of a current page and then click to the next page, saves HTML body etc. I tried this: var result = []; var scrapeAndClick = function() { $("div.ui-paginator.ui-paginator-top.ui-widget-header.ui-corner-top").each(function() { result.push( $(this).html() ); //klikej na dalsi stranky var nextButton = $('a.ui-paginator-next.ui-state-default.ui-corner-all'); console.log('Click next button'); nextButton.click().delay(4000) }); }; scrapeAndClick(); In Google Chrome console it returns me only the HTML body of the first page. APIFY does not return anything. Can someone see, where is the problem? If is someone interested in the whole Page function: async function pageFunction(context) { const { log } = context; const searchSelector = 'div.ui-panel-content.ui-widget-content > button'; //vyber "Gemeenschappelijk Landbouw Beleid" z Kies subsidie: const subsidySelector = $("span.column2 > select.jsTruncate").val("10000"); log.info('Select CAP ') subsidySelector //klikni na Zoek log.info('Click search.') $(searchSelector).eq(0).click() //loopujeme dalsi stranky a ukladame html body var result = []; var scrapeAndClick = function() { $("div.ui-paginator.ui-paginator-top.ui-widget-header.ui-corner-top").each(function() { result.push( $(this).html() ); //klikej na dalsi stranky var nextButton = $('a.ui-paginator-next.ui-state-default.ui-corner-all'); console.log('Click next button'); nextButton.click().delay(4000) }); }; scrapeAndClick(); return result; } StartURL is this: https://mijn.rvo.nl/europese-subsidies-2017 I found an old question on APIFY forum (https://forum.apify.com/t/clickable-link-that-doesnt-change-the-url/361/3), however it seems that it was done on old version of APIFY crawler. Thanks a lot for any help!
Cannot communicate between captivate html and parent html
I have a published captivate html file that is loaded into an iframe of another html. I cannot communicate between the two, not even with localStorage. Can anyone tell me what I'm missing? Parent html var everythingLoaded = setInterval(function () { if (/loaded|complete/.test(document.readyState)) { clearInterval(everythingLoaded); init(); } }, 10); function init() { ScormProcessInitialize(); var studentID = ScormProcessGetValue("cmi.core.student_id"); var student_name = ScormProcessGetValue ("cmi.core.student_name"); var nameArraya = student_name.split(" "); var nameArrayb = nameArraya[1].split(","); var studentNumber = nameArrayb[0]; ScormProcessSetValue("cmi.core.lesson_status", "incomplete"); localStorage.setItem("_studentNumber", studentNumber); alert("Student Number: " + studentNumber + " Student Mame: " + student_name); setTimeout(function () { document.getElementById("iFrame_a").innerHTML = "<iframe name='iframe_1' id='frame_1' src='//somepath.com/sandbox/somecourse/index.html' frameborder='0' width='1000px' height='605px'></iframe>"; }, 250); } function sendComplete() { alert("Send from index start!"); ScormProcessSetValue("cmi.core.lesson_status", "completed"); alert("send status: Completed"); } window.onbeforeunload = function (){ cpInfoCurrentSlide = localStorage.getItem("_cpInfoCurrentSlide") alert(cpInfoCurrentSlide); if(cpInfoCurrentSlide >= 40) { alert("onbeforeunload called: " + cpInfoCurrentSlide ) ScormProcessSetValue("cmi.core.lesson_status", "completed"); } } iframe code snippet localStorage.setItem("_cpInfoCurrentSlide", cpInfoCurrentSlide);
I believe your problem is with onbeforeunload. As I remember captivate packages clobber any functions associated with onbeforeunload in the parent frame when they load. Try this instead, override your SCORM api setvalue method: var oldLMSSetValue = window.API.LMSSetValue; window.API.LMSSetValue = function(key, value){ if(key === 'cmi.core.lesson_status' && value === 'completed'){ //do your stuff here cpInfoCurrentSlide = localStorage.getItem("_cpInfoCurrentSlide") alert(cpInfoCurrentSlide); } //call the original scorm api function so that it runs as expected. oldLMSSetValue(key,value); }; edit: this code would go in the parent window, not the iframe.
TinyMCE: j is undefined
What's wrong with this code? I get "J is undefined message" after insert the image. I think this happends while i try to close itself. var ImageDialog = { init : function() { var f = document.forms[0], ed = tinyMCEPopup.editor; e = ed.selection.getNode(); if (e.nodeName == 'IMG') { f.src.value = ed.dom.getAttrib(e, 'src'); } }, update : function() { var f = document.forms[0], nl = f.elements, ed = tinyMCEPopup.editor, args = {}, el; tinyMCEPopup.restoreSelection(); if (f.src.value === '') { if (ed.selection.getNode().nodeName == 'IMG') { ed.dom.remove(ed.selection.getNode()); ed.execCommand('mceRepaint'); } tinyMCEPopup.close(); return; } tinymce.extend(args, { src : f.src.value }); el = ed.selection.getNode(); if (el && el.nodeName == 'IMG') { ed.dom.setAttribs(el, args); tinyMCEPopup.editor.execCommand('mceRepaint'); tinyMCEPopup.editor.focus(); } else { ed.execCommand('mceInsertContent', false, '<img src="'+args['src']+'" id="_mce_temp_rob" alt="" />', {skip_undo : 1}); ed.undoManager.add(); ed.focus(); ed.selection.select(ed.dom.select('#_mce_temp_rob')[0]); ed.selection.collapse(0); ed.dom.setAttrib('_mce_temp_rob', 'id', ''); tinyMCEPopup.storeSelection(); } tinyMCEPopup.close(); }, getImageData : function() { var f = document.forms[0]; this.preloadImg = new Image(); this.preloadImg.src = tinyMCEPopup.editor.documentBaseURI.toAbsolute(f.src.value); } }; tinyMCEPopup.onInit.add(ImageDialog.init, ImageDialog);
it's a tinymce bug. Internally, the tinymce code uses a <span id="mce_marker"></span> to remember the caret-position when pasting. when validating the resulting fragment, after the paste, the span is deemed invalid and removed, thus breaking the code by removing the marker. This issue will be fixed in the next official tinymce minor release. There are some workarounds for this kind of issue. One is to add to add id and mce-data-type attribute to spans as valid elements (init setting). Example: // The valid_elements option defines which elements will remain in the edited text when the editor saves. valid_elements: "#[id|class|title|style]," + "a[name|href|target|title]," + "#p,-ol,-ul,-li,br,img[src],-sub,-sup,-b,-i,-u" + "-span[data-mce-type]",