WebEraser

Erase parts of any webpage --annoyances, logos, ads, images, etc., permanently with just, Ctrl + Left-Click.

目前為 2017-01-04 提交的版本,檢視 最新版本

// ==UserScript==
// @name        WebEraser
// @version     1.3.4
// @namespace   sfswe
// @description Erase parts of any webpage --annoyances, logos, ads, images, etc., permanently with just, Ctrl + Left-Click.
// @include     *
// @require     https://code.jquery.com/jquery-3.1.1.js
// @require     https://code.jquery.com/ui/1.12.1/jquery-ui.js
// @resource    whiteCurtains      https://github.com/SloaneFox/imgstore/raw/master/whiteCurtainsDbl.jpg
// @resource    whiteCurtainsOrig  https://github.com/SloaneFox/imgstore/raw/master/whiteCurtains.orig.jpg
// @resource    whiteCurtainsXsm   https://github.com/SloaneFox/imgstore/raw/master/whiteCurtainsExSm.jpg
// @resource    whiteCurtainsTrpl  https://github.com/SloaneFox/imgstore/raw/master/whiteCurtainsTrpl.jpg
// @icon        https://github.com/SloaneFox/imgstore/raw/master/WebEraserIcon.jpg
// @run-at      document-start
// @author      Sloane Fox
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// ==/UserScript==

 
//
// History
// updated Jan  2017   v1.3.4  Bug fixes.  Issue with load sequence on Chrome.  Monitoring class changes relating to identifier of element.
// updated Dec  2016   v1.3.0  Bug fixes, check for duplicate selectors, color & other ui issues.  Removed zoomer (it used up a little cpu).
// updated Nov  2016.  v1.2.3  Iframe handling for deep iframes.
// updated Oct  2016.  v1.2.2  Fixed bug, GM menu on Chrome not closing.
//                     v1.2.1  Adapted for use also in Google Chrome/Chromium web browser.
// updated Sept 2016.  v1.2    Added user option to turn on the monitoring for new nodes (node mutations).

//timer(); 

var iframe=window!=window.parent, border_width=6;
var environ=this;

if (!chromeInit()) $(main.bind(environ));

var win=window,
    host=window.document.location.host,
    pathname=window.document.location.pathname, webpage=host+pathname, website=host;
var elems_to_be_hid=getHidElems(), askedAlready, gelem, gelems, gpre_elem,
    bblinker, promptOpen, rbcl="sfswe-redborder", pbcl="sfswe-prevborder", tbcl="sfswe-transparentborder";
var tab="&emsp;&emsp;&emsp; &emsp; "; // tab=5spaces, emsp=4spaces, but HTML tab in a <pre> wider hence extra emsp's.
var curtain_icon=getValue("ownImageAddr","")||GM_getResourceURL("whiteCurtains"), curtain_cnt=0,
    curtain_slim_icon=getValue("ownImageAddr","")||GM_getResourceURL("whiteCurtainsOrig"),
    curtain_xslim_icon=getValue("ownImageAddr","")||GM_getResourceURL("whiteCurtainsXsm"),
    curtain_wide_icon=getValue("ownImageAddr","")||GM_getResourceURL("whiteCurtainsTrpl"),
    ignoreIdsDupped;

var zaplists=new zaplist_composite(), overlay=false;
var config=getValue("config",{keepLayout:"checked",monitor:{}}); if (!config.monitor) config.monitor={};

Number.prototype.in=function(){for (i of Array.from(arguments)) if (this==i) return true;}; // Use brackets with a literal, eg, (2).in(3,4,2);
Number.prototype.inRange=function(min,max){ if (this >=min && this<=max) return true;}; // Ditto.
Number.prototype.withinRangeOf=function(range,target){ return this.inRange(target-range,target+range); }; // Ditto.
String.prototype.truncate=function(maxsize) { if (this.length<=maxsize+5) return this; else return this.slice(0,maxsize/2)+"..."+webpage.slice(-maxsize/2); }; //sumarize with ellipsis
//thousand's comma, call Number.toLocaleString()
//if (iframe) console.log=x=>null;       //logger(); // Logs from doc start.

document.addEventListener("scroll", function(e){ if (!overlay) return; e.preventDefault();e.stopPropagation();e.stopImmediatePropagation();},true);

//window.addEventListener("click",handleClick,true);
window.addEventListener("mousedown",handleClick,true);
window.addEventListener("message", postMessageHandler,false);

function main() {
    if (iframe) return;
    init_jquery();
    inner_eraseElements();
    var nerased=$(".Web-Eraser-ed").length;
    setTimeout(x=> {
	var forErasure=getHidElems(0,0,"cnt_for_erasure");
	if (nerased < forErasure) { inner_eraseElements(); }
	else if ($(".Web-Eraser-ed").length==0 && elems_to_be_hid)
	    console.info("WebEraser message: no match for any selectors:",getHidElems(),"\nWebpage:",webpage);
	var nerased2=$(".Web-Eraser-ed").length;
	nerased=(nerased-1) + (nerased2-nerased);
//	if (config.monitor[website]) setTimeout(observeThings,2000*nerased);
    }, 2000+300*nerased);
    
    GM_addStyle( jqueryui_dialog_css         //GM_getResourceText ("jqueryUiCss")
		+".sfswe-prevborder { border-color:transparent !important;border-width:"+border_width+"px !important;border-style:double !important; }"
		+".sfswe-transparentborder  { border-color:transparent !important;border-width:"+border_width+"px !important;border-style:double !important; }"
		+".sfswe-redborder { border-color:red !important; border-width:"+border_width+"px !important;border-style:double !important; }"
	       ); // A later defined rule has precedence when both rules in effect.
    //setTimeout(inner_eraseElements,1500);
    if (chrome && window.submenuModule) submenuModule.register("WebEraser","w");
    GM_registerMenuCommand("Erase Web Elements ["+(elems_to_be_hid?"some erased":"none erased")+"]", eraseElementsCmd,"","", "E");
    setTimeout(reattachTornCurtains,4000);
    gelems=$();
} //main()
		
function keypressHandler(event) { try{ //while prompt is open.
    var ip=$("#sfswe-seledip:enabled");
    if (ip.length) { //live typing of selector.
	setTimeout(ip=>{
	    var cval=ip.val(), matched_els=[];
	    try{matched_els=$(cval);} catch(e) {};// bad selector, transient
	    if (matched_els.length) { // may unwind.
		highlightElement(0,"off",null,"mere_highlight"); 
		highlightElement($(cval),null,null,"mere_highlight"); }
	    else highlightElement(0,"off","restore");
	},500,ip);
    } else  { // widen/narrow
	switch(event.key) {
	case "w": widen(); break;
	case "n": narrow(); break; 
	default: return; } 
	return false;
    }
} catch(e) {console.error("An key handler error:"+e+" "+e.lineNumber);} };

function handleClick(e,iframe_click) {  //try { //called from event handler in page & iframe, and pseudo called from click within iframe.
    //console.log("WebEraser Click handler",e,iframe_click);
    if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
	if (e.preventDefault) {e.preventDefault();   e.stopPropagation();}
	if (!iframe_click) {
	    var seltext_len=window.getSelection().toString().length;
	    window.status="webEraser, Ctrl-Click, on HTML element:"+e.target.tagName+" "+seltext_len+", ifame: "+iframe;
	    if (seltext_len != 0) return;
	    if (e.target.blur) e.target.blur();
	    if (iframe)  {
		window.parent.postMessage("sfswe-iframe-click","*"); // msg,origin
		return false;
	    }
	} // endif !iframe_click
	var permrm, target=e.target;
	while (/HTMLUnknownElement/.test(target.toString())) target=target.parentNode; //Avoid non HTML tags.
	if ($(target).is(".WebEraserCurtain")) {
	    let reply=confirm("This will completely remove selected item, continue?");
	    if (reply) openCurtains("zap",$(target).siblings("img").addBack());
	}
    else if (!askedAlready) {
	checkIfPermanentRemoval(target)
	    .then(function(permrm){
		if (permrm==false)      { askedAlready=true; alert("You hit TEMP, ctrl-click from now until page is reloaded will merely remove elements from the page temporarily. ");}
		if (permrm!=undefined)   inner_eraseElements("from Click");         //undefined==>escape (cancel)
	    });
    }
    else target.style.setProperty("display","none","important");
    return false;
    //} catch(e) { console.error("Click handling error:"+e+" "+e.lineNumber,logStack()); }
};  //handleClick()

function postMessageHandler(e){ //reads postMessage().
    if (e.data!="sfswe-iframe-click") return;
    if (iframe) {
	window.parent.postMessage("sfswe-iframe-click","*");
	return;
    }
    var iframeEl=$("iframe").filter(function(){ return this.contentWindow==e.source; });
    handleClick({target:iframeEl[0],ctrlKey:true},"iframe_click");
}

function sprompt(tex,initv,cancel_btn="Cancel",ok_btn="OK"){ // returns a promise with true/false value or for prompts an array value: [true/false,string], rejected with escape.
    var dialog, p=new Promise((resolve,reject)=>{
	dialog=sprompt_inner(tex,initv,resolve,reject,cancel_btn,ok_btn);
    });
    p.dialog=dialog;
    return p;
}
function sconfirm(msg,cancelbtnText,okbtnText) { return sprompt(msg,undefined,cancelbtnText,okbtnText); }
function salert(msg) { return sprompt(msg,undefined,-1,"OK"); }

function sprompt_inner(pretext,initval,resolve,reject,cancelbtnText,okbtnText) { // "Cancel" has reply of false or null (if a prompt), "OK" gives reply of true or "", Escape key returns undefined reply.  undefined==null is true. but not for ""
    var that=arguments.callee; if (that.last_dfunc) that.last_dfunc("destroy"); // Only one modal allowed.
    var input_tag, input_style="width:80%;font-size:small;";
    var confirm_prompt=initval===undefined;
    if (!confirm_prompt) input_tag=initval.length<40 ? "input" : (input_style="width:95%;height:100px;","textarea");
    var content=$("<div class=sfswe-content tabindex=2 style='outline:none;white-space:pre-wrap;background:#fff0f0;'>"
		  +"<div>"+pretext+"</div>" 
		  +(initval!==undefined ? "<"+input_tag+" spellcheck='false' style='"+input_style+"'  tabindex='1'></"+input_tag+">":"")+"</div>");
    content.find("input:not(:checkbox),textarea").val(initval);
    content.resizable();
    var sp1=$(document).scrollTop();
    var dfunc=content.dialog.bind(content);
    var dialog=content.dialog({
	modal: true, width:"auto", //position: { my: "center", at: "center", of: window }, // Greater percent further to top.
	buttons: {
	    [cancelbtnText]: function(e) { if (confirm_prompt) resolve(false); else resolve([false, $(this).find("input,textarea").val()]); dfunc("close"); return false;},
	    [okbtnText]: function(e) { if (confirm_prompt) resolve(true); else resolve([true,$(this).find("input,textarea").val() || ""]); dfunc("close"); return false;}
	},
	close: function(e) { dialog.off("keydown"); $(document).scrollTop(sp1); if (e.key=="Escape") reject("Escape");}
    }).parent();
    if (cancelbtnText==-1) { dialog.find("button").each(function(){   if (this.textContent=="-1") $(this).remove(); }); }
    dialog.wrap("<div class=sfswe-sprompt></div>"); // allows css rules to exclude other jqueryUi css on webpage from own settings, a
    dialog.keydown(function(e){	if (e.key == "Enter" && !/textarea/i.test(e.target.tagName)) $("button:contains("+okbtnText+")",this).click();  }); 
    dialog.css({"z-index":2147483647, width:550, position:"fixed", left:200, top: 50, background: "whitesmoke"}); //"#fff0e0"
    dialog.find(".ui-dialog-titlebar").remove(); // No img in css for close 'x' at top right so remove.  Title bar not in normal confirm anyhow.
    dialog.draggable("option","handle", ".ui-dialog-buttonpane"); //
    dialog.resizable();
    content.css({"overflow-x":"hidden","max-height":innerHeight-dialog.position().top-$(".ui-dialog-buttonpane").height()}).scrollTop(0);
    setTimeout(function(){var ips=dialog.find("input,textarea");if (ips.length) ips.focus(); else content.focus();},100);
    that.last_dfunc=dfunc;
    return dialog; //.ui-dialog
}

function checkIfPermanentRemoval(target) {   // called from click handler.
    var confirm_promise, checkif_resolve,
	checkif_promise=new Promise((resolve)=>{
	    checkif_resolve=resolve;
	    var parent=target.parentNode, index=0;
	    var msg="Permanently erase selected element(s) &mdash; now seen on page red bordered and blinking?  Use 'w' and 'n' keys freely, to widen and narrow your selection.  "
		+"Escape quits.  Enter OK's.  Use the GM menu <a href='#abc"+Math.random().toString(36)+"'>Erase Web Elements</a> to edit internal code." // Clickable link see .click below.
    		+"Hit Temp button below for ctrl-click to erase element(s) temporarily and inhibit this prompting until reload."
		+"\n\nInternal code for <span id=fsfpe-tagel></span><br><div style='display:inline-block; position:relative;width:100%'><input disabled id=sfswe-seledip style='width:80%;margin:10px;'><div id=sfswe-seledipfull style='position:absolute; left:0; right:0; top:0; bottom:0;'></div></div>";
	    $(document).keypress(keypressHandler);
	    confirm_promise=sconfirm(msg,"Temp","OK");
	    var dialog=confirm_promise.dialog;
	    dialog.find(".ui-dialog-buttonpane").prepend("<input id=sfswe-checkbox6 type=checkbox style='margin-top:0;margin-left:3px; float:left;'>"
							 +"<label style='vertical-align: text-bottom; display:inline; margin-left:4px;'>Completely delete element.</label>");
	    dialog.find("a").click(e=>{
		dialog.trigger(jQuery.Event("keydown",{keyCode:27,key:"Escape"})); // close prompt.
		eraseElementsCmd();});
	    var input=$("#sfswe-seledip"), ip=input[0], div_surround=input.next();
	    div_surround.click(e=>{ // a click on input & surround enables it.
		ip.disabled=false; 	    ip.setSelectionRange(999,999);
		div_surround.css("display","none");
		input.focus();
		input.blur(e=>{ip.disabled=true; div_surround.css("display",""); });
	    }); //
	    highlightElement(target);
	    setTimeout(function(){dialog[0].scrollIntoView();},100);
	});//new Promise()
    close_of_prompt(confirm_promise, checkif_resolve);
    return checkif_promise;
}		       //end checkIfPermanentRemoval()

function close_of_prompt(confirm_promise,checkif_resolve) {
    var nested_confirm, first_reply, complete_rm;
    nested_confirm=confirm_promise.then(function(reply){
	$(document).off('keypress');
	$(":data(pewiden-trace)").data("pewiden-trace",""); // remove trace
	var complete_rm=$("#sfswe-checkbox6:checked").length!=0;
	if (reply) reply=$("#sfswe-seledip").val().trim(); 
	else { highlightElement(0,"off","restore");checkif_resolve(false); return; };
	confirm_promise.data=[reply,complete_rm]; // use ES6 await?
	if (reply)  {
	    let ancErased=$(reply).closest(".Web-Eraser-ed");
	    if (hidElementsListCmd("isthere?", reply) || ancErased.length) { alert("Already attempting erasure of the element specified or parent, if not being erased properly try ticking the monitoring option or open 'Erase Web Elements' GM menu and hit its 'OK' button.\nInternal code:"+reply+"\n\n   Ancestor:"+nodeInfo(ancErased)); return; }
	    //let {keep_layout,zap}=zaplists.which(reply); if (complete_rm==zap) alert("already same erasure type")//else hidElementsListCmd("rm",reply);
	    return sconfirm("Click 'Site', or press Enter, to "+(complete_rm ? "completely " : "")+"erase from any page visited on this website: \n\t"+website
		     +".\n\nOr click 'Page' button to erase selected elements from webpage:\n\t"+webpage
			   +".\n\nInternal code for Element:\n\t"+reply,"Page","Site");
	} else { //reply==""
	    highlightElement(0,"off","restore"); 
	    //  checkif_resolve(false);
	}
    });// confirm_promise.then
    nested_confirm.then(function(reply2) {
	var [reply, complete_rm]=confirm_promise.data;
	if (reply2)
	    hidElementsListCmd("add",reply+" site");
	else  
	    hidElementsListCmd("add",reply);    // btn1 -> null, btn2 -> "<string>" null==undefined
	if (hidElementsListCmd("rm", $(reply).find(".Web-Eraser-ed"))) console.info("Removed child selectors of",reply);
	highlightElement(0,"off","restore");
	if (complete_rm) zaplists.add(reply);
	checkif_resolve(true);
    }); //then()
    nested_confirm.catch(function() {
	highlightElement(0,"off","restore");
    });
}

function eraseElementsCmd() { 
    // Called from GM script command menu and from clickable within ctrl-click  prompt.
    // 
    var sitewide, erasedElems, page_erasedElems=[], site_erasedElems=[], no_sels;
    erasedElems=getHidElems("withSite");
    no_sels = !erasedElems ? 0 : erasedElems.split(/,/).length;
    var prompt_promise=sprompt(
	"See checkboxes distantly below to set the script's configutation values.  Ctrl-click is the usual way to erase parts of a webpage, however, as an alternative below you can manually "
	    +"edit the internal selectors for erased elements, eg, 'DIV#main_column site', optional word 'site' erases the element at the entire website.  " 
	    +"For more than one selector use commas to separate"+(no_sels?", currently there's "+no_sels+" below":"")+".  To remove all element erasures set to blank.  Reload webpage if necessary."
	, erasedElems.replace(/,/g,", \n"));
    prompt_promise.then(function([btn,reply]){
	if (!btn) return; //cancel ==> null, undefined==> escape. (null is == to undefined!)
	config={monitor:config.monitor}; delete config.monitor[website];
	if ($("#sfswe-checkbox:checked").length)	config.noAnimation="checked";
	if ($("#sfswe-checkbox2:checked").length)	config.keepLayout="checked";
	if ($("#sfswe-checkbox3:checked").length) 	config.hideCurtains="checked";
	if ($("#sfswe-checkbox5:checked").length)       config.monitor[website]="checked";
	if ($("#sfswe-checkbox4:checked").length) {
	    toggleCurtains();
	    let subpromt=sprompt("Please enter http address of curtain image to be used.  If giving left and right images separate with a space.  "
				 +"Leave empty to reset.  Accepts base64 image strings.","");
	    subpromt.dialog.attr("title","Perhaps try a quaint example; one found with an image search for 'curtains':\n\thttp://www.divadecordesign.com/wp-content/uploads/2015/09/lace-curtains-5.jpg");
	    subpromt.then(function([btn2,reply2]){
		if (btn2) { setValue("ownImageAddr",reply2); curtain_icon=reply2||GM_getResourceURL("whiteCurtains"); curtain_slim_icon=reply2||GM_getResourceURL("whiteCurtainsOrig"); curtain_wide_icon=reply2||GM_getResourceURL("whiteCurtainsTrpl");
			    $(".WebEraserCurtain").attr("src",curtain_icon);  }
		toggleCurtains(); });
	} else {
	    let duplicates={};
	    reply=reply.replace(/\s*,\s*/g,",").replace(/(?=[^,])\n(?=[^,])/g,",").split(/,/); // , newline->comma if none; if no comma all is put in [0]
	    $(reply).each((i,str)=>{ //
		if (str=="") return;
		if (duplicates[str]) return;
		duplicates[str]=true;
		str=str.trim();
		if (/\ssite$/.test(str)) site_erasedElems.push(str.replace(/\ssite$/,""));
		else page_erasedElems.push(str);
	    });
	    try{$(reply);} catch(e){alert("Bad selector given."); throw(e);}
	    setValue("config",config);
	    setValue(website+":erasedElems",site_erasedElems.toString());
	    setValue(webpage+":erasedElems",page_erasedElems.toString());
	    zaplists.update();
	    openCurtains();
	    $(".Web-Eraser-ed").each(function(){
		var self=$(this);
		self.css({display: self.data("sfswe-display"), visibility: self.data("sfswe-visibility")});
		self.removeClass("Web-Eraser-ed");
	    });
	    $(".WebEraserCurtains").remove();
	    setTimeout(inner_eraseElements,1000,"fromPrompt"); //'cos openCurtains takes time
	    //inner_eraseElements("fromPrompt");
	}
    });//then()
    var keep_layout=config.keepLayout;
    var dialog=prompt_promise.dialog;
    dialog.find(".ui-dialog-buttonpane").prepend(
	"<div class=sfswe-ticks style='width:78%;float:left;font-size:10px;'>"
	    +"<input id=sfswe-checkbox2 type=checkbox style='float:left;"+(!keep_layout?" margin:0 3px;":"")+"' "+keep_layout+"><label>Preserve layout (in general).</label>"
	    +(keep_layout ? "<input id=sfswe-checkbox3 type=checkbox style='margin:0 3px 0 10px;height:12px;'"+(config.hideCurtains||"")+"><label>Hide curtains (when preserving layout).</label>" : "")
	    +"<br><input id=sfswe-checkbox type=checkbox style='margin-left:3px;'"+(config.noAnimation||"")+"><label>Disable animation (in general).</label>"
	    +"<br><input id=sfswe-checkbox4 type=checkbox style='margin-left:3px;'><label>Set your own curtains' image.</label>"
	    +"<input id=sfswe-checkbox5 type=checkbox style='margin-left:15px;'"+(config.monitor[website]||"")+"><label>Monitor for new elements on this website.</label>"
	    +"</div>"
    );
    dialog.find("input:checkbox").css({"-moz-appearance":"none",height:12});
    dialog.find(".ui-dialog-content").attr("title","WebEraser userscript.\n"+webpage+"\n\nCurrent matches at this webpage:\n"+bodymsg());
} //eraseElementsCmd()

function inner_eraseElements(from) { 
    //
    // Called at page load and when user sets selector(s) for erasure.
    // 1. Go through uncurtained elements for erasure and do curtainClose (or css display to none) on each.
    // 2. Class each as "Web-Eraser-ed" and backup css values that might get changed.
    // 3. If changes were made log details to console and to logging div.
    //
    var erasedElems=getHidElems(), len=erasedElems.length, erasedElems_ar=erasedElems.split(/,/), count=0, nomatch=[];
    if (erasedElems_ar[0]=="") erasedElems_ar.shift(); //fix split's creation of array length one for empty string.
    var theErased=$(".Web-Eraser-ed"); theErased.removeClass("Web-Eraser-ed");
    erasedElems_ar.forEach(function(sel,i){
	erasedElems=$(sel); //Array.from(document.querySelectorAll(sel)); //$(sel), jQ cannot find duplicate ids.
	erasedElems.each(function() {
	    var eld=this,el=$(eld); // 40msecs per 'each' loop.
	    markForTheCurtains(el,eld,sel);
	    var no_anima=config.noAnimation, keep_layout=config.keepLayout;
	    if (no_anima && !keep_layout)  eld.style.setProperty("display","none","important");
	    else  if (el.css("display")!="none") closeCurtains(el, no_anima, measureForCurtains);
	    count++;
	}); //erasedElems.each()
	if (erasedElems.length==0) nomatch.push(sel);
    }); //forEach()
    if (iframe) return;
    theErased=$(".Web-Eraser-ed");
    observeThings();

    if (len==0)  observeThings("off");
    if (theErased.length==0) return;  ////////////////////
    if (nomatch.length) console.info("WebEraser message: no match for some selectors:",nomatch,"at",webpage);
    var ieemsg="Userscript WebEraser is using selectors to hide "+count+(count==1 ? " element that was":" elements that were")+" present on page at site: "+website
        +".\nSee GM menu command Erase Web Elements to check and edit selector list.  "
	+(config.keepLayout ? "" : "Keep layout is not ticked.")
	+(config.noAnimation ? "Animation is off." : "")
	+(config.hideCurtains ? "Hide curtains is ticked." : "");
    theErased.each(function(i){
	var that=$(this);
	var sel=that.attr("selmatch-sfswe");
	var onzaplist=zaplists.which(sel); // 10 msecs to here from prev in  closeCurtains() above.
	var is_an_overlay=that.prev().hasClass("sfswe-overlay");
	ieemsg+="\n"+(i+1)+":"+sel;
	ieemsg+=".\t\t"
	    +(is_an_overlay ? "=> Considered as an Overlay,takes up > 2/3 of window, deleted."
	      : onzaplist.zap ? " => complete erasure."
	      : onzaplist.keep_layout ? " => erase but keep layout."
	      : "" );
    });
    count=0;
    console.info(ieemsg);
    bodymsg(ieemsg.replace(/(.*\n){2}/,""),"init");
}

function closeCurtains(el, noAnimKeepLayout, finishedCB=x=>x) { // called from inner_eraseElements()
    //console.log("closeCurtains1",el,noAnimKeepLayout,finishedCB,"sel:",el.attr("selmatch-sfswe"),"\n\nLog of Stack",logStack());
    var that=closeCurtains; if (!that.final_curtain) that.final_curtain=0;
    var hide_curtains=config.hideCurtains, keep_layout=config.keepLayout;
    var old_curtained=el.prev().data("covered-el");
    if ( ! old_curtained || ! old_curtained.is(el))
	var [curtainRod,lrcurtains]=createCurtains(el,noAnimKeepLayout);
    else { var curtainRod=el.prev(), lrcurtains=curtainRod.children();}
    curtainRod.css("display","");
    var onzaplist=zaplists.which(el); // 20 msecs from prev
    if (noAnimKeepLayout) {
	lrcurtains.css({width:"51%"});
	if (onzaplist.zap) { curtainRod.css({display:"none"}); el[0].style.setProperty("display","none","important");} // "none" triggers monitor if on.
	else if (onzaplist.keep_layout||hide_curtains||curtainRod.hasClass("sfswe-overlay")){
	    curtainRod.css({visibility:"hidden",display:""});
	    el[0].style.setProperty("visibility","hidden","important");
	}
	measureForCurtains();
    }
    else { // Do animated curtain closing, then, perhaps, fade out.
	that.final_curtain++;
	manimate(lrcurtains,["width",51,"%"],2000,10,function(){ ///////////////////////Animation
	    el=$(this).closest(".WebEraserCurtains").data("covered-el");
	    if (!keep_layout || curtainRod.hasClass("sfswe-overlay")||onzaplist.zap) {
		el.add(curtainRod).delay(200).fadeOut(
		    500, function(){
			this.style.setProperty("display","none","important"); // triggers monitor if on.
			if (el[0]==this && --that.final_curtain==0) finishedCB();
		    });
	    }
	    else if (hide_curtains||onzaplist.keep_layout) {
		el.add(curtainRod).delay(200).fadeOut(
		    1000, function(){
			this.style.setProperty("visibility","hidden","important");
			this.style.setProperty("display",$(this).data("sfswe-display"),"important"); //triggers monitor.
			//curtainRod.css({visibility:"hidden",display:""});
			curtainRod.remove();
			if (el[0]==this && --that.final_curtain==0) finishedCB();
		    });
	    } else if (--that.final_curtain==0) finishedCB();
	}); //animate()
    }
    return false;
} //closeCurtains()

function getSelectorWithNearestId(target,exclude_classes) {
    var sel, nearestNonNumericId=target.closest(":regexp(id,^\\D+$)").attr("id"), nnmi=nearestNonNumericId; //closest also checks target
    //console.log("nnmi",nnmi, "matches #els:",$("[id="+nnmi+"]").length);
    if (nnmi && $("[id="+nnmi+"]").length>1) { nnmi="";ignoreIdsDupped=true;} // Page error duplicate ids, ignore id.
    if (nnmi) nnmi=$("#"+nnmi).prop("tagName")+"#"+nnmi; //cos of jQ & multiple ids.
    if ($(nnmi).is(target)) sel=nnmi;
    else {
	sel=selector(target,$(nnmi),true,0,exclude_classes); //ok if nnmi is undefined id.
	if (!sel) sel=nnmi; //both target and $(nnmi) are same element. 
	else if(nnmi) sel=nnmi+sel;
    }
    return sel;
}

function getHidElems(withsite, justpels_ar, cnt){
    var els, pels=getValue(webpage+":erasedElems","").trim(), sels=getValue(website+":erasedElems","").trim();
    if (cnt) return getHidElems().split(/,/).reduce((prev_res,sel)=>prev_res+$(sel).length,0);
    if (withsite && sels) {
	sels=sels.replace(/,/g," site,")+" site"; // see reverse of this in hidElementsListCmd() and  eraseElementsCmd().
    }
    if (justpels_ar) return pels.split(","); //webpage elements.
    return pels + (sels && pels ? "," : "") + sels;
}

function hidElementsListCmd(cmd,str,str2) {
    console.log("hidElementsListCmd, cmd:",cmd, "str:",str,"str2:",str2, "HidElems:",getHidElems());
    var page_erasedElems=getValue(webpage+":erasedElems","").trim(),
	site_erasedElems=getValue(website+":erasedElems","").trim(), sitewide;
    switch(cmd) {
    case "add":
	if (hidElementsListCmd("isthere?",str)) return;
	if (/\ssite$/.test(str)) { sitewide=true; str=str.replace(/\s+site$/,"");  }
	if (sitewide)  site_erasedElems += site_erasedElems ? ","+str : str;
	else  page_erasedElems += page_erasedElems ? ","+str : str;
	$(str).each(function() {  $(this).data("sfswe-oldval", $(this).css(["display","visibility","height","width"]));	});
	break;
    case "mv":
	if (hidElementsListCmd("rm",str)) str2+=" site";
	hidElementsListCmd("add",str2);
	return; //return needed to prevent saving of old values.
    case "rm":
	if (str instanceof jQuery) { str.each(function(){ hidElementsListCmd("rm", $(this).attr("#selmatch-sfswe"));	});  return str.length; }
	page_erasedElems=$.map(page_erasedElems.split(/,/),el=>el==str ? null : el.trim()).join(",");
	site_erasedElems=$.map(site_erasedElems.split(/,/),el=>el==str ? null : el.trim()).join(",");
	break;
    case "isthere?": //check if str is amongst hidden elements list.
	return getHidElems().split(/,/).includes(str);
	//return getHidElems().split(/,/).reduce((prev_res,next)=>prev_res||next==str,false);
    }
    
    console.log("hidElementsListCmd END, site_erasedElems:",site_erasedElems,"\n","page_erasedElems",page_erasedElems);
    setValue(website+":erasedElems",site_erasedElems);
    setValue(webpage+":erasedElems",page_erasedElems);
    zaplists.update();
    return sitewide;
}

//Blinks are double, one for selected elements, other is only when at top/bottom of narrow/widen chosen.
function highlightElement(elem, off, restore, mere_highlight) { //also updates prompt with elem's selector.
    if (!off) { // on
	elem=$(elem);
	if (elem.length==0) return;
	gpre_elem=gelem;
	gelem=$(elem);
	var newsel,fullsel,h=gelem.height(),w=gelem.width();
	console.info("widen/narrow, element to highlight is",gelem, mere_highlight);
	if (!mere_highlight) { // not typed in but from widen/narrow etc.
	    var selinput=$("#sfswe-seledip"),            //sfs_pesel");
		elhtml=gelem[0].outerHTML.replace(gelem[0].innerHTML,"");
	    
	    newsel=getSelectorWithNearestId(gelem,tbcl+" "+rbcl+" Web-Eraser-ed");
	    fullsel=selector(gelem,0,false,0,tbcl+" "+rbcl+" Web-Eraser-ed");
	    gelems=$(newsel).not(gelem);
	    selinput.val(newsel); //+"<pre style='font-size:14.4px;'>\n\tHTML in pre</pre>");
	    selinput.prop("title", (newsel!=fullsel ? "Full selector:\n\n\t"+fullsel+"\n\n" : "")
			  //+gelem[0].outerHTML.replace(/>.*/g,">").replace(/\s*</g,"<")
		    +"Element html:\n"+elhtml
		    +"\n\nElement style:\n"+myGetComputedStyle(gelem[0]));
	} //endif !mere_highlight
	updatePromptText(newsel,fullsel);
	gelem.data("pewiden-trace","true"); //    if (!gelem.hasClass("pewiden-trace"))
//!!	gelem.parents().addBack().addClass(tbcl);
//	gelem.find(">:only-child").addClass(tbcl);
	gelem.add(gelems).toggleClass(rbcl);
	gelem.elh=gelem[0].style.height;	gelem.elw=gelem[0].style.width;
	gelem.height(h- 2*border_width);gelem.width(w- 2*border_width);
	bblinker=setInterval(function(){ // normal "selected" blink.
	    if (gelems.length) gelems.toggleClass(rbcl);
	    else gelem.toggleClass(rbcl);    //.css({borderColor:"red",borderWidth:"9px",borderStyle:"double"});
	},1200);
    }
    else { //off
	clearInterval(bblinker);
	gelem.removeClass(rbcl);
	gelem[0].style.height=gelem.elh;	gelem[0].style.width=gelem.elw;
	//gelem.height(h+ 2*border_width);gelem.width(w+ 2*border_width);
	if (restore) $("."+tbcl).removeClass(tbcl); 
    }
}

function widen() { // .html() return &gt; encodings, .text() does not.  tab as @emsp must be set with html() not text()
    var selinput=$("#sfswe-seledip");
    if (/[:.][^>]+$/.test(selinput.val())) {
	var newsel=selinput.val().trim().replace(/[:.][^:.]+$/,"");
	selinput.val(newsel);
	gelems=$(newsel);
	gelems.addClass(rbcl);
	updatePromptText();
	return;
    }
    if (gelems.length) { gelems.removeClass(rbcl);gelems=$();}
    var p=gelem.parent();
    if (p.is("html")) {
	blinkBorders(gelem); //blink double indicates top of hierarchy.
	return;
    }
    highlightElement(0,"off");
    highlightElement(p);
}

function narrow() {
    if (gelems.length) {
	widen();  // nulls gelems.
	narrow(); // Follow gelem trace back to el.
	//narrow();
	return;
    }
    var trace=gelem.find(":data(pewiden-trace):first"); // trace left by highlightElement()
    if(trace.length==0) trace=gelem.find(">:only-child");
    if (trace.length==0) {
	blinkBorders(gelem);
	return;
    }
    highlightElement(0,"off");
    highlightElement(trace);
}

function updatePromptText(newsel,fullsel) { 	// set text size tagname etc.
    var updated_text="";
    if (gelems.length<=1)
	updated_text="selected ("+gelem.prop("tagName").toLowerCase()+") element ("+(gelem.height()|0)+"x"+(gelem.width()|0)+"pixels)";
    else
	updated_text="selected "+gelems.length+" "+gelem.prop("tagName").toLowerCase()+"s";
    updated_text+=":";
    $("#fsfpe-tagel").parent().prop("title","Click here for widen/narrow function.\nClick on the internal code below, then move mouse a small bit to see "
				    +(newsel!=fullsel ? "full position in hierarchy," : "")
		    +" html and style settings of the selected element. ");
    $("#fsfpe-tagel").text(updated_text);
}

function myGetComputedStyle(el) {
    if (!document.defaultView.getDefaultComputedStyle) return ""; // has no getDefaultComputedStyle().
    var roll="",defaultStyle=document.defaultView.getDefaultComputedStyle(el);
    var y=document.defaultView.getComputedStyle(el), val, val2, i=1;
    for (let prop in y) {
	if (/^[a-z]/.test(prop) && ! /[A-Z]/.test(prop) && (val=y[prop])
	    && val!=defaultStyle[prop]) {
	    if (val.trim)  //just a type check
		if (val.startsWith("rgb")) val="#"+val.replace(/[^\d,]/g,"").split(/,/).map(x=>Number(x).toString(16)).join("");
	    if (prop.startsWith("border") && y[prop.replace(/-\w*$/,"")+"-style"]=="none") continue; // Error in getDefaultComputedStyle borders not set properly (eg, color should be that of el)
	    roll+= prop +": "+val+"; ";
	    if (i++%3==0) roll+="\n";
	} //endif
    }
    return roll;
}


function blinkBorders(elem, interval=150, times=4) { // borders must already be set.
    times*=2;
    var cnt=0,i=setInterval(function(){
	cnt++;
	elem.toggleClass(rbcl);
	//!!
//	elem.toggleClass(tbcl); 
	if (cnt==times) {clearInterval(i);elem.removeClass(rbcl);}// interference so rm class.
    },interval);
}

function init_jquery() {
    // if (!iframe) setTimeout(x=>{ console.log("Using jquery version, $.fn.jquery:",$.fn.jquery,", jquery-ui version $.ui.version:",$.ui.version);
    // 				 $("head").append("<script>if (window.$) console.log('UnsafeWindow jquery version:',$.fn.jquery,'.',$.ui?$.ui.version:'  No jqui.');else console.log('UnsafeWindow is not using jQuery.')</script>");},4000);
    $.fn.reverse = Array.prototype.reverse; 
    $.fn.swap = function(to) {
	var a=this.eq(0), b=$(to).eq(0);
	var tmp = $('<span>').hide();
	a.before(tmp);
	b.before(a);
	tmp.replaceWith(b);
	return;
    };
    $.easing["stepper"] =  function (x, t, b, c, maxt) { // eg, see, console.log($.easing)  for other funcs.
	// var y=c*(t/=maxt)*t + b;
	// if (x<0.4) y=0.1;
	//console.log(x);
	//return y;
	return x;
    };
    $.extend($.expr[':'], {
	regexp: function(currentobj, i, params, d) { //filter type function.
	    params=params[3].split(/,/);       //eg, [ 'regexp', 'regexp', '', 'className,promo$' ]
	    var attr=params[0], re=params[1];  //eg, className, promo$
	    if (attr=="class") attr="className";
	    var val=currentobj[attr]+""||"";
	    if (attr=="className") return val.split(/\s/).some(function(cl){return cl.match(re);});
	    else return val.match(re);
	}});     //usage eg: $(“div:regexp(className,promo$)”);
    (function($){ 
	$.event.special.destroyed = {
	    remove: function(o) {
		if (o.handler) {
		    o.handler();
		}
	    }
	}; })(jQuery); //Usage: $("#anid").bind('destroyed', function() {// do stuff}) // only for is jQ  removed el.
} //init_jquery()

function selector(desc,anc,no_numerals,recursed,exclude_classes) { try { // descendent, ancestor, such that ancestor.find(ret.val) would return descendant.  If no ancestor given it gives it relative to body's parent node.   // See example usage in checkIfPermanentRemoval(). Numeraled classes/ids are excluded.
    anc=$(anc).eq(0); //apply only to first ancestor.
    if (anc.length==0) anc=$(document.body.parentNode); // !anc wouldnt work for a jq obj.
    desc=$(desc);
    if ( (desc.closest(anc).length==0 || desc.length!=1) && !recursed) {
	console.info("Too many elements or descendant may not related to ancestor:");
	console.info("Descendant is:"+selector(desc,0,0,true));
	console.info("Ancestor is:"+selector(anc,0,0,true)+".");
	return;
    }
    // Last element is highest in node tree for .parentsUntil();
    var sel=
	    desc.add(desc.parentsUntil(anc)) // up to but not including.
	    .reverse() // see above, needs: $.fn.reverse = Array.prototype.reverse; 
            .map(function() { // works from bottom up to ancestor, hence need for reverse().
		var t=$(this), tag=this.tagName.toLowerCase(), nth=t.prevAll(tag).length+1, id="", cl, nthcl;
		//id=this.id.replace(/^\s*\b\s*/,"#"); if (!ignoreIdsDupped) id="";
		cl=t.attr("class"); // Don't use this.className (animated string issue)
		cl=this.className.replace(/^\b|\s+(?=\w+)/gi, ".").trim();
		if (exclude_classes) cl=cl.replace(RegExp(".("+exclude_classes.replace(/ /g,"|")+")","g"),"");
		if (no_numerals && /\d/.test(id)) id="";
		if (no_numerals && /\d/.test(cl)) cl="";
		if ( (cl && t.siblings(tag+cl).length==0)
		     || id
		     || t.siblings(tag).length==0)
		    nth=0;
		else if (cl && t.siblings(tag+cl).length!=0) {
		    cl+=":eq("+t.prevAll(tag+cl).length+")";   //jQuery only has :eq()
		    nth=0;
		}
		return tag+(nth?":nth-of-type("+nth+")":"")+id+cl; ////////////////////nth-of-type is One-indexed.
	    }) //map()
            .get()         //
	    .reverse()
            .join(">");
    if (desc.is(anc.find(">"+sel))) {
	if (anc.is(document.body.parentNode)) return "html>" + sel;
	return ">"+sel;
    } else {
	console.info("Selector result:\n\t"+sel+"  Not findable in ancestor, nor in body's parent.");
	if ($(sel).length) return sel; //Its the very top element, <HTML>.
    } } catch(e){console.log("Can't get selector for",desc);fixBadCharsInClass(desc);}
}

function fixBadCharsInClass(obj) { //official chars allowed in class, throw error in jquery selection.
    obj.parents().addBack().each(function(){ this.className=this.className.replace(/[^\s_a-zA-Z0-9-]/g,""); });
}

function markForTheCurtains(el,eld,sel,unmark) {
    if (!unmark) {
	el.css({overflow:"hidden"}).addClass("Web-Eraser-ed").attr("selmatch-sfswe",sel) //hidden, so height not 0.
	    .data({sfsweDisplay: eld.style.display, sfsweVisibility:eld.style.visibility, sfsweOverflow: eld.style.overflow}); // needed in case zero height element with floating contents. // To make it have dims, in case of zero height with sized contents.
    }
    else el.css({overflow:el.data("overflow")}).removeClass("Web-Eraser-ed").attr("selmatch-sfswe",""); //hidden, so height not 0.
    console.log("markForTheCurtains, el:",nodeInfo(el),unmark);
    
}

function reattachTornCurtains(curtains=$(".WebEraserCurtains")) {
    var torn=false;
    curtains.each(function(){
	//console.log("reattachTornCurtains set el(covered) to:",$(this).data("covered-el"));
	var that=$(this), el=that.data("covered-el");
	if (el.parent().length==0 || !el.hasClass("Web-Eraser-ed")) {
	    torn=true;
	    that.addClass("sfswe-delete","true");
	    //that.remove();
	}    });
    $(".sfswe-delete").remove();
    if (torn) inner_eraseElements();
}

function measureForCurtains(curtains=$(".WebEraserCurtains")) {
    curtains.each(function(){
	//console.log("measureForCurtains set el(covered) to:",$(this).data("covered-el"));
	var that=$(this), el=that.data("covered-el"); //that.next(); // next is the covered elem.
	
	var w=el.outerWidth(), h=el.outerHeight()+1; // Includes padding & border, margin included if 'true' passed.  jQuery sets and unsets margin-left during this, provoking attrModifiedListener.
	if (!el.hasClass("Web-Eraser-ed")) {
	    el.addClass("Web-Eraser-ed");
	    el.css({overflow:"hidden"});
	}
	var offset=moffset(el);
	that.css(offset).css({height:h,width:w});
    });
}

function bodymsg(str,init) {
    if ($("#sfswe-div-logger").length==0) $("body").append("<pre style='display:none;' id=sfswe-div-logger><pre class=init></pre></pre>");
    var sfsprelog=$("#sfswe-div-logger");
    var initpre=sfsprelog.find(".init");
    if (str) if (init) initpre.text("\n"+str+"\n");        //b.attr("sfswe-message",str);bodymsg.init=str;}
    else {
	if (str==bodymsg.str) 	sfsprelog.append(".");
	else {
	    sfsprelog.append("\n"+str);
	    console.info("WebEraser Monitor: "+str);
	    bodymsg.str=str;
	}
    }
    return initpre.text();
}

function observeThings(disable) { // call will start or if running reset monitoring, with param, it disables.
    var that=arguments.callee; that.off=[];
    if (that.obs1) { try { that.obs1.disconnect(); that.obs2.disconnect();} catch(e){
	console.log("Error during turn off of observations,",e);  } }
    if (disable || ! config.monitor[website]) return;

    var a,b,sels=getHidElems(),
	nomonitor=set=>{ if (set==1) { that.off.push(true); a=that.obs1.takeRecords(); b=that.obs2.takeRecords();
				       //if(a.length ||b.length) console.log("TOOK records");
				     } // jquery get causes set, hence inf.loop.
			 if (set==0) { that.off.pop(); a=that.obs1.takeRecords(); b=that.obs2.takeRecords();
				       //if(a.length ||b.length) console.log("0TOOK records");
				     } return that.off.slice(-1)[0]; };
    
    var parseCssText=str=>JSON.parse("{" + (str||"").replace(/[\w-]+(?=:)/g,'"$&"').replace(/:\s*(.+?)(?=;)/g,':"$1"').replace(/;/g,",").slice(0,-1) + "}");
    console.info("WebEraser message: Monitoring elements that match given selectors for creation and display and to be erased on sight.");
    $(sels).each((i,el)=>$(el).data("sfswe-oldval", $(el).css(["display","visibility","height","width"])) ); //copy of style obj but dead (eg, cssText not updated).
    obs1_connect(sels);
		 
    function obs1_connect(selectors) {
	that.obs1=attrModifiedListener(document,selectors,["style","class","id"],function(mutrecs) {
	    if (nomonitor()) return;
	    nomonitor(1);
	    var rec=mutrecs[0], t=rec.target, target=$(t), attr=rec.attributeName;
	    var oldval=target.data("sfswe-oldval"), currval=target.css(["display","visibility","height","width"]);
	    
	    //console.log("Attr modified: "+attr,	"\n\nmut.oldValue--attr currvalue\n\n    ",			rec.oldValue,"\n\n ---",nodeInfo(target.attr(attr)),"\n\n\ntarget.data.oldvals:\t\t",			nodeInfo(oldval),"\n\nCurvals from .css():\t\t",nodeInfo(currval),			"\n\n\ntarget",target,"\n\nAll "+mutrecs.length+" All mutation records with oldvals:\n",			mutrecs.map(x=>"\noldval: "+x.oldValue+"\t\t\t\tnode: "+nodeInfo(x.target)).join(" ")  );
	    //ldval=parseCssText(mutrecs[0].oldValue);	    //var moldval=parseCssText(rec.oldValue);
	    
	    var objsel=target.attr("selmatch-sfswe");
	    if (!objsel) {
		target.data("sfswe-oldval", target.css(["display","visibility","height","width"]));
		markForTheCurtains(target,t,findMatchingSelector(target,selectors));
	    }
	    if (!oldval && /class|id/.test(attr)) { //&& target.prev("sfswediv")[0]) {
		var newlen=that.obs1.add(target);
		oldval={};
	    }
	    
	    if (currval.display=="none" && oldval.display!="none") {
		bodymsg("change-nodisplay:"+target.attr("selmatch-sfswe"));
		target.prev().css("display","none");
		measureForCurtains();
	    } else if (currval.display!="none" && (oldval.display=="none" || oldval.display==undefined )) {
		bodymsg("change-display:"+target.attr("selmatch-sfswe"));
		target.prev().css("display","");
		closeCurtains(target); //,true); //no animation since asynch anime will trigger too many mutation records.
	    }
	    if ( parseInt(currval.height)|0 - parseInt(oldval.height)|0) {
		bodymsg("change-height:"+nodeInfo(target)+" "+currval.height);
		measureForCurtains();
	    } else if ( parseInt(currval.width)|0 - parseInt(oldval.width)|0) {
		bodymsg("change-width:"+nodeInfo(target)+" "+currval.width);
		measureForCurtains();
	    } else if (currval.visibility!=oldval.visibility)
		bodymsg("change-visibility:"+nodeInfo(target));
	       // 	if (currval.visibility=="visible")  {
	       // 	    bodymsg("change-display:"+target.attr("selmatch-sfswe"));
	       // 	    target.prev().css("display","");
	       // 	    closeCurtains(target,true); //no animation since asynch anime will trigger too many mutation records.
	       // 	} else if (currval.visible!="visible") {
	    target.data("sfswe-oldval",currval);
	    // change-visibility?
	    //}); //forEach
	    nomonitor(0);
	}); // attrModifiedListener(... 
    } // obs1_connect()
    that.obs2=nodeMutationListener(document,sels, function(foundArrayOfNodes, parentOfMutation,removed) {
	if (nomonitor()) return;
	nomonitor(1);
	foundArrayOfNodes.forEach(node=>{   // A flattened subtree, if node was again removed quickly it may have no parent.
	    var jQnode=$(node);
	    if (!removed) { // new node inserted.
		jQnode.data("sfswe-oldval", jQnode.css(["display","visibility","height","width"]));
		var foundsel=findMatchingSelector(jQnode,sels);
		bodymsg("new-node:"+foundsel);
		markForTheCurtains(jQnode,node,foundsel);
		closeCurtains(jQnode,false,measureForCurtains); //nomonitor(0); },300);
	    } else { // node removed
		//if(jQnode.attr("cc"))  {
		bodymsg("node-delete:"+jQnode.attr("selmatch-sfswe"));
		$(".WebEraserCurtains[cc='"+jQnode.attr("cc")+"']").remove(); //.filter(function(){return $(this).data()})
		measureForCurtains();
		//} 
	    }
	});//forEach
	nomonitor(0);
    },true); //nodeMutationListener()
}

function findMatchingSelector(obj,sels) {
    return sels.split(/,/).find(sel=>obj.is(sel));
}

function openCurtains(zap_or_keep="",curtains=$(".WebEraserCurtain")) { // called from ctrl-click with curtains, eraseElementsCmd() w/o curtains, and lrcurtains.click sets "keep"
    //console.log("openCurtains",zap_or_keep);
    setTimeout(function() {
	manimate(curtains,["width",0,"%"],3500,20,function() {
	    var that=$(this), erased_el=that.parent().next();
	    var sel=erased_el.attr("selmatch-sfswe");
	    bodymsg("opened curtains for sel:"+sel+", cc:"+erased_el.attr("cc"));
	    switch(zap_or_keep[0]) {
	    case "z": zaplists.add(sel);erased_el.css("display","none");measureForCurtains();console.info("Completely erased,",sel+".");break; 
	    case "k": zaplists.add(sel,"keep");;erased_el[0].style.setProperty("visibility","hidden","important");console.info("Hidden for layout,",sel+".");break; //keep_layout
	    case "t": that.parent().css("display","none");break;           //tzap
	    case "a": hidElementsListCmd("rm",sel); observeThings(); that.parent().remove();markForTheCurtains(erased_el,0,0,"unmark"); break; //azap
	    }
	    //erased_el.prev().css({display:"none"});
	});
    },1000);
}

function createCurtains(el, noAnimKeepLayout) {
    var h=el.outerHeight()|0,w=el.outerWidth()|0, area=h*w, iw=w/2, pos= moffset(el),    
	warea=window.innerHeight*window.innerWidth, csspos=el.css("position");
    //console.log("createCurtains ",noAnimKeepLayout,"h/w",h,w," el:",el);
    // 9 msecs to here from function start.
    var lsrc=curtain_icon.split(/\s+/)[0], rsrc=curtain_icon.split(/\s+/).slice(-1); //last string
    if (!getValue("ownImageAddr","")) switch(true) {
    case w<250:  lsrc=rsrc=curtain_xslim_icon;break;
    case w<500:  lsrc=rsrc=curtain_slim_icon;break;
    case w>800: lsrc=rsrc=curtain_wide_icon;break; }
    var lcurtain=$("<img class='WebEraserCurtain sfswe-left' style='left:0;position:absolute;height:100%;' src="+lsrc+"></img>"), 
	rcurtain=$("<img class='WebEraserCurtain sfswe-right' style='right:0;position:absolute;height:100%;' src="+rsrc+"></img>"), 
	curtainRod=$("<sfswediv tabindex=0 class=WebEraserCurtains cc="+(++curtain_cnt)+" style='z-index:2147483640; position:absolute; display:block; overflow:hidden;opacity:0.94;'></sfswediv>"), //inline is default here, 'd take full width of parent.
	lrcurtains=lcurtain.add(rcurtain), sel=el.attr("selmatch-sfswe");
    el.attr("cc",curtain_cnt);
    curtainRod.append(lcurtain,rcurtain);
    curtainRod[0].title="Shift-Click to hide and preserve page layout.\nCtrl-click to persistently delete from layout.\nAlt-Click to remove erasure.\nDouble click to open or close curtains.\nClick to focus and enable typing of 'w', for widen, 'n', for narrow, 'l', lighten."
	+"\n\nSelector is: "+sel+".";
    
    lrcurtains.click(function({ctrlKey:ctrl,shiftKey:shift,altKey:alt,target:target}) {
	if (!(alt||shift)) return;
	//console.log("target",target,$(target).parent().next().attr("selmatch-sfswe"));
	if (ctrl&&shift) alert("Curtained selector is,"+$(target).parent().next().attr("selmatch-sfswe")); 
	else if (shift) openCurtains("keep_layout",lrcurtains);
	else if (alt) openCurtains("azap",lrcurtains);
	curtainRod.focus(); 
	return false;
    });
    lrcurtains.dblclick(e=>openCurtains("tzap",lrcurtains));
    el.dblclick(e=>closeCurtains(el)); el.mousedown(e=>false);  el.mouseup(e=>false);  el.click(e=>false);
    curtainRod.keypress(moveRod);
    curtainRod.css({height:h,width:w}).css(pos).data({coveredEl:el,selmatchSfswe:sel});
    lrcurtains.css({ width: (!noAnimKeepLayout ? 0 : "51%" )}); // Initial width of each curtain.
    var portions=area/warea*100|0;    //curtainRod.attr("init-calc",(calc|0)+" "+portions);
    if (portions>=60) { //>75% of window is covered.
	var visible_area;
	with (Math) {visible_area=min(w,window.innerWidth)*min(h,window.innerHeight);}
	if (visible_area>=warea*0.6) {
	    lcurtain.css({left:"10%"});
	    curtainRod.css({width:"80%",top:"10%"}).addClass("sfswe-overlay");
	    lrcurtains.css({height:h*0.8});
	    setTimeout(x=>$("html, body").css("overflow",(i,v) => v=="hidden" ? "auto": null).css("position",(i,v) => v=="fixed" ? "static": null),4000);
	    overlay=true;
	    //First event listener can stop prop to ones added later, ideally would be added at doc-start.
	    console.info("This is an Overlay (>2/3 covered, "+portions+"%, "+h+"x"+w+"): ", sel, el);}}
    el.before(curtainRod); ////////////////////
    return [curtainRod,lrcurtains];
}

function moveRod(e) {
    if (e.key=="w"||e.key=="n") {
	let  newel, rod=$(this), el=rod.data("covered-el"), p=el.parent(), newsel, oldsel=el.attr("selmatch-sfswe");
	var trace=el.find(":data(pewiden-trace):first"); 
	if(trace.length==0) trace=el.find(">:only-child");
	
	if (e.key=="n")   newel=trace; //narrow
	else              newel=p;     // widen
	if (newel.length==0 || newel.is("body")) {	rod.focus();$("body").blur();rod.focus(); return false;}
	
	newsel=selector(newel,0,false,0,"Web-Eraser-ed");
	el.data("pewiden-trace","true");
	hidElementsListCmd("mv", oldsel, newsel);
	newel.before(rod);
	rod.data("covered-el",newel);
	markForTheCurtains(el,null,null,"unmark");
	markForTheCurtains(newel,newel[0],newsel);
	rod[0].title=rod[0].title.replace(/\nSelector is:.*\./,"\nSelector is:"+newsel+".");
	measureForCurtains();
	rod.focus();
    } else if (e.key=="l") { //lighten
	var rod=$(this);
	var op=rod.css("opacity");
	rod.css("opacity",op*0.8);
	setTimeout(x=>rod.css("opacity",rod.css("opacity")*1.25),10000);
    } else if (e.key=="s") {
	var sfsprelog=$("#sfswe-div-logger");
	sfsprelog.css("display","");
	sfsprelog[0].scrollIntoView();
    }
    
    return false;
}

function toggleCurtains() {
    var that=arguments.callee; 
    $(".WebEraserCurtains").each(function(){
	if (!that.xor)    {manimate($(".WebEraserCurtain",this),["width",51,"%"],2000,12);}
	else              manimate($(".WebEraserCurtain",this),["width", $(this).data("init-width"),"%"],4000,8);
    });
}

function zaplist_composite() { // composite pattern
    if (iframe) return;
    var zlists=[new zaplist(webpage),new zaplist(website),new zaplist(webpage,"kl"),new zaplist(website,"kl")];
    this.add=function(sel,keep_layout){ 
	zlists.forEach(function(el) { el.add(sel,keep_layout);});   };
    this.contains=function(el){  // may be a dom/jq object or a string selector.   
	return zlists.some(function(list) { return list.contains(el);}); };
    this.which=function(el) { // The 2 bits returned tell if & on which zaplist the elem is.
	if (this.contains(el)) {
	    var has_keep_layout=zlists.map(v => v.contains(el)).includes("kl");
	    return {keep_layout:has_keep_layout,zap:!has_keep_layout};
	}
	return {keep_layout:false,zap:false};
    };
    this.update=function(sel){	zlists.forEach(function(el) { el.update();});  };
    this.toString=()=>"[object zaplist_composite]";
}

function zaplist(key,keytype) {
    var fullkey=key+":zaplist"+(keytype? ":"+keytype : "");
    var savelist=function() { setValue(fullkey,list); };
    var readlist=function() { return getValue(fullkey,[]); }; 
    var list=readlist(); 
    //console.log("zaplist created key:",key,", keytype",keytype,", list",list);
    
    this.add=function(str,kl) {
	if (!!kl != !!keytype) return;
	if(getValue(key+":erasedElems","").split(/,/).includes(str)) {
	    list.push(str);
	    savelist();
	}
    };
    this.contains=function(jqobjOrStr){
	if (list.length==0) return;
	if (jqobjOrStr.attr) jqobjOrStr=jqobjOrStr.attr("selmatch-sfswe");
	if (list.indexOf(jqobjOrStr) != -1)
	    return keytype||"zap";
    };
    this.rm=function(str) {
	var i=list.indexOf(str);
	if (i!=-1)   list.splice(i,1);
	savelist();
    };
    this.update=function() {
	if (list.length==0) return;
	var strs_ar=getHidElems().split(/,/);
	list=list.filter(function (lel) {
	    return strs_ar.includes(lel);
	});
	savelist();
    };
    this.toString=()=>"[object zaplist]";
}

function moffset(elem, eld=elem[0]) {
    if (elem.css("position").includes("fixed")) 
	return Object.assign(elem.position(),{position:"fixed"});
    var dominPar=elem.offsetParent()[0]; 
    return left_top(elem);

    function left_top(elem) {
	var {left,top}= elem.position(); // something sets & unset margintop or left during something here for some reason, margins and floating els may disaffect calc!
	let margl=parseInt(elem.css('margin-left')), margt=parseInt(elem.css('margin-top'));
	//let bordl=parseInt(elem.css('border-left-width')), bordt=parseInt(elem.css('border-top-width'));
	var x = left + margl, y = top + margt;
	do {
	    elem = elem.offsetParent();
	    if (elem.is(dominPar) || elem.is("html")) break;
	    let {left,top}=elem.position(); // something sets & unset margintop during something here for some reason, margins and floating els may disaffect calc!
	    x += left; y += top;
	} while (true)
	if (y) y--;
	return { left: x, top: y };
    }
}	      

//
// MutationObserver functions.           Eg, var obs=nodeInsertedListener(document,"#results", myCBfunc);  function myCBfunc(foundArrayOfNodes, DOMparentOfMutation);
// Requires jQuery.
// See https://www.w3.org/TR/dom/#mutationrecord for details of the object sent to the callback for each change.
// Four functions available here:
// Parameter, include_subnodes is to check when .innerHTML add subnodes that do not get included in normal mutation lists, these lower nodes are checked when parameeter is true.
// Return false from callback to ditch out.
 
function nodeInsertedListener(target, selector, callback, include_subnodes) {
    return nodeMutation(target,selector,callback,1, include_subnodes);
}
function nodeRemovedListener(target, selector, callback, include_subnodes) {
    return nodeMutation(target,selector,callback,2, include_subnodes);
}
function nodeMutationListener(target, selector, callback, include_subnodes) { //inserted or removed, callback's 3rd parameter is true if nodes were removed.
    return nodeMutation(target,selector,callback,3, include_subnodes);
}
function attrModifiedListener(target, selectors, attr, callback) { //attr is array or is not set.  Callback always has same target in each mutrec.
    var attr_obs=new MutationObserver(attrObserver), jQcollection=$(selectors);
    var config={ subtree:true, attributes:true, attributeOldValue:true};
    if (attr) config.attributeFilter=attr;      // an array of attribute names.
    attr_obs.observe(target, config);
    function attrObserver(mutations) {
	var results=mutations.filter(v=>{ return $(v.target).is(selectors)||$(v.target).is(jQcollection);});
	if (results.length) { //Only send mutrecs together if they have the same target and attributeName.
	    let pos=0;
	    results.reduce((prev_res,curr,i)=>{ if ( prev_res.target!=curr.target || prev_res.attributeName != curr.attributeName) {
		callback(results.slice(pos,i)); pos=i;  } // not really a reduce!
		return curr; 
	    });
	    callback(results.slice(pos)); //////////////////<<<<<<<
	} }
    attr_obs.add=function(newmem) { jQcollection=jQcollection.add(newmem); return jQcollection.length; };
    return attr_obs;
}

//
// Internal functions:
function nodeMutation(target, selectors, callback, type, include_subnodes) { //type new ones, 1, removed, 2 or both, 3.
    var node_obs=new MutationObserver(mutantNodesObserver);
    var jQcollection, cnt=0;
    node_obs.observe(target, { subtree: true, childList: true } );
    return node_obs;
    
    function mutantNodesObserver(mutations) { 
	var sel_find, muts, node;
	jQcollection=$(selectors);
	for(var i=0; i<mutations.length; i++) {
	    if (type!=2) testNodes(mutations[i].addedNodes, mutations[i].target); // target is node whose children changed
	    if (type!=1) testNodes(mutations[i].removedNodes, mutations[i].target,"rmed"); // no longer in DOM.
	}
	function testNodes(nodes, ancestor, rmed) { //non jQ use, document.querySelectorAll()
	    if (nodes.length==0) return;
	    var results=[], subresults=$();
	    for (var j=0,node; node=nodes[j], j<nodes.length;j++) {
		if (node.nodeType!=1) continue;
		if (jQcollection.is(node)) results.push(node);
		if (include_subnodes) subresults=subresults.add($(node).find(jQcollection));
	    }
	    results=results.concat(subresults.toArray());
	    if (results.length) callback(results, ancestor, rmed);
	} //testNodes()
    };
} 
//
// End MutationObserver functions.  Usage example, var obs=nodeInsertedListener(document,"div.results", myCBfunc);  function myCBfunc(foundArrayOfNodes, ancestorOfMutation);
//

function manimate(objs,[css_attr,target_val,suffix],interval,noOf_subintervals,CB) { // CB is invoked once, at end.  $.animate max-ed out cpu for 30 secs or so.
    //console.log("manimate()",css_attr,"currval:",objs.css(css_attr),target_val,interval,noOf_subintervals,CB,objs);
    var len=objs.length,cnt=0,i,random_element=3;
    if (!len) return false;
    var maxi=objs.length-1, subinterval=interval/noOf_subintervals,
	init_int=parseInt(objs[0].style[css_attr]), // assume same initital position and same units/suffix for all objs.
	m=(target_val-init_int)/noOf_subintervals,
	linear=(v,i)=>init_int+m*(i+1),	// quad=(v,i)=>Math.min(target_val_int,init_int+(5/3)*Math.pow(i+1,2)-(5/3)*(i+1)),	// combo=(v,i)=>quad(v,i)/2+linear(v,i)/2,
	plotvals=new Uint32Array(noOf_subintervals).map(linear);
    subinterval+=random(-subinterval/random_element,subinterval/random_element),  /// Random element +/- 1/random_element.
    i=setInterval(eppursimuove,subinterval,objs);
    function eppursimuove(that) {
	objs.css(css_attr,plotvals[cnt]+suffix);
	if (++cnt==noOf_subintervals) {     
	    clearInterval(i);  CB && CB.call(that);}}
}

function setValue(n,v) { return GM_setValue(n,JSON.stringify(v)); }
function getValue(n,v) { return JSON.parse(GM_getValue(n,JSON.stringify(v))); }

function random(min,max) {
    return Math.floor(Math.random() * ((max+1) - min)) + min;
}

function timer() { //console.time() and console.timeEnd() not working at the mo, so tstamp sent with each console.log
    //if (window!=window.parent || timer.log) return;
    if (timer.log) return; // aleady started
    var originalLogger = console.log;
    timer.log=originalLogger;
    console.log = function () {
	if (!timer.begin) {
	    timer.begin=Date.now();
	    timer.last_time=timer.begin;
	    originalLogger.call(timer.begin,">>>>Init timer "+location.pathname+":");
	}
	var args=Array.from(arguments);
	var tstamp=Date.now();
	var sdiff=tstamp-timer.begin, ldiff=tstamp-timer.last_time;
	args.unshift(sdiff+"ms, "+ldiff+"ms\t");
	timer.last_time=tstamp;

	originalLogger.apply(this, args);
    };
}

function logger() {
    $(document).dblclick(outputlogger);
    var originalLogger = console.log;
    logger.log=originalLogger;
    console.log = function () {
	if (!logger.this) logger.this=this;
	// Do your custom logging logic
	var argq=$(document).data("loggerq");
	var args=Array.from(arguments);
	if (!argq) argq=[];
	if (document.readyState!=logger.state) {
	    argq.push(document.readyState+":");
	    logger.state=document.readyState;
	}
	argq.push(args);
	$(document).data("loggerq",argq);
	
	args.push(document.readyState);
	originalLogger.apply(this, args);
    };
} //logger()

function csscmp(prevval, newval) { try{
    var that=arguments.callee;
    var covered={}, roll="";
    for (let i in prevval) {
	covered[i]=1;
	if (newval[i]===undefined) roll+="Removed: "+i+"="+prevval[i]+" ";
	else if (prevval[i]!=newval[i]) roll+="Changed: "+prevval[i]+" to: "+newval[i]+" ";
    }
    for (let i in newval) if (!covered[i]) roll+="Added: "+i+"="+newval[i]+" ";
    return roll||"Same";
}catch(e) {console.error("csscmp Error",e.lineNumber,e);}}

function nodeInfo(node1,plevel,...nodes) { // show DOM node info or if name/value object list name=value
    //console.log("nodeInfo stack:",logStack());
    if (node1==undefined || node1.length==0) return;
    plevel=plevel||1;
    if (isNaN(plevel) && plevel) { nodes.unshift(node1,plevel); plevel=1; }
    else nodes.unshift(node1);
    plevel--;
    return nodes.map(node=> {
	if (!node || typeof node=="string") return node;
	if (node && node.attr) node=node[0];
	if (node && node.appendChild) {
	    let classn=node.className ? node.className.replace("Web-Eraser-ed","") : "";
	    return node ? node.tagName.toLowerCase() + classn.replace(/^\b|\s+(?=\w+)/gi, ".").trim() + (node.id||"").replace(/^\s*\b\s*/,"#")
	    + (plevel>0 ? "<" + nodeInfo(node.parentNode,plevel):"")
	    : "<empty>";
	}
	else if (node && node.cssText) return node.cssText;
	else
	    return ""+Object.entries(node)      // entries => array of 2 member arrays [[member name,value]...]
	    .filter(x=> isNaN(x[0]) && x[1] )  //Only name value members of object converted to string.
	    .map(x=>x[0]+":"+x[1]).join(", ");
    }).join(" ");
}
			  //selector(node,node.parentNode,0,0,"Web-Eraser-ed").replace(/^html>body>/,""); }

function outputlogger() {
    var originalLogger=logger.log;
    var that=logger.this;
    
    var argq=$(document).data("loggerq");
    originalLogger.call(that,"===============Logger Output==========================");
    argq.forEach(function(v){
	originalLogger.call(that,v); //this changes in forEach in this case!
    }); // originalLogger.apply(this,argq);
    originalLogger.call(that,"===============End Logger Output=======================");
    return false;
};

function logStack(fileToo) { // deepest first.
    var res="", e=new Error;
    var s=e.stack.split("\n");
    if (fileToo) res="Stack of callers:\n\t\t"; //+s[1].split("@")[0]+"():\n\t\t"
    for (var i=1;i<s.length-1;i++)
	res+=s[i].split("@")[0]+"() "+s[i].split(":").slice(-2)+"\n";
    return !fileToo ? res : {Stack:s[0]+"\n"+res}; 
}

function Ppositions(el, incl_self,not_pos_break="") { 
    el=$(el); var roll="\n\n";
    var els=el.parents();
    if (incl_self) els=els.add(el).reverse();
	els.each(function(){
	var pos=$(this).css("position");
	roll+=this.tagName+" "+pos+"\n";
	if (! pos.includes(not_pos_break)) return false;
	//       /^((?!relative).)*$/   matches any string, or line w/o \n, not containing the str "relative"
    });
    return roll;
}

var jqueryui_dialog_css=(
    ".ui-dialog-content,.ui-dialog,.ui-dialog textarea { font-size: 12px; font-family: Arial,Helvetica,sans-serif; border: 1px solid #757575; "
	+"background:whitesmoke; color:#335; padding:12px;margin:5px;} "
	+".ui-dialog-buttonpane {  background:whitesmoke; font-size: 10px; cursor:move; border: 1px solid #ddd; overflow:hidden; } "
	+".ui-dialog-buttonpane button { background: #f0f0e0; }"
	+".ui-dialog-buttonset { float:right; } "
	+".ui-widget-overlay { background: #aaaaaa none repeat scroll 0 0; opacity: 0.3;height: 100%; left: 0;position: fixed;  top: 0; width: 100%;}"
	+".ui-button,.ui-widget-content { text-align:left; color:#333; border: solid 1px #757575; padding: 6px 13px;margin: 4px 3px 4px 0;} "
	+".ui-corner-all,.ui-dialog-buttonpane {border-bottom-left-radius:30px;}"
	+".ui-button:hover { background-color: #ededed;color:#333; } "
	+".ui-button { background-color: #f6f6f6; color:#333; }"
	+".ui-dialog {position:absolute;padding:3px;outline:none;}"
	+".ui-resizable-handle { position:absolute; cursor: url(data:image/svg+xml;base64,"
	+"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAAAAACo4kLRAAAACXBIWXMAAAsTAAALEwEAmpwYAAABAUlEQVQY022RsWrCYACEvz9EK1HQCilYu+jWdNKlj9BNN1/Cp/IBFHTMExihdTCIm0sR26W6KNHkvw6lunjLjffdnXn7fG0/PzzeG0A/m+/Ve/REB3CL/laStn7RBTpOG0iTXgWg0ktSoO20DJDv5gBy3TxgWkQFAG+QSdnAAyhErMtuFSiN0nRUAqpuec2x2V/4YOr7fd2AH/ebR+zhbMOaCWJrF4GphfZ8sEhSNm3EVrJxY5pJkhGAkjtzNRxuyAGws2Ap0DKYWYDbQRek3e6K9A8/TNPhBf5mzbEBvDCTpCz0ADN25gJOkzPAeXICNL85spu8/N0B8LDafK0+ouQXfemVYVtdIewAAAAASUVORK5CYII="
	+") 10 10, row-resize; } .ui-resizable-sw {bottom:5px;left:5px;}"
	+".ui-resizable-w, .ui-resizable-e { width:10px;height:100%;top:-5px;} .ui-resizable-n, .ui-resizable-s { width:100%;height:10px;} .ui-resizable-n {top:-5px; } .ui-resizable-w {left:-5px; } .ui-resizable-e {right:-5px; }"
  	+ (str=>str+str.replace(/-moz-/g,"-webkit-"))(
	    ".sfswe-content :-moz-any(div,input) { font-size:13px;padding:6px;margin:4px 3px 4px 0;color:#333; opacity:1; backgound:whitesmoke; }"
		+".sfswe-content :-moz-any(span) { font-size:13px;padding:0;margin:0;color:#333;}"
		+".sfswe-content :-moz-any(a,a:visited)    { color:#333;text-decoration:underline; padding:0;margin:0;}"
    		+".sfswe-content :-webkit-any(a,a:visited) { color:#333;text-decoration:underline; padding:0;margin:0;}"
	)  +".sfswe-content a:hover {opacity:0.5;}"
    	+".ui-tooltip { font-size: 7px; }"
	+".sfswe-ticks * {font-size:11px;padding:0px;margin:2px;}")
	.replace(/\.ui/g,".sfswe-sprompt .ui"); //gives namespace of .sfswe-prompt

function chromeInit() {
    environ.chrome=false;
    
    //console.log("ChromInit, environ:",environ.toString()," this:", this.toString(),		"\n\nGM_getValue: environ?",environ.GM_getValue,		"\n\nwindow.gm? ",window.GM_getValue, "this.gm, ",this.GM_getValue 	       );

    if ( !environ.GM_getValue || "Barychelidae"!=GM_getValue("arachnoidal","Barychelidae")){ //chromium
	console.info("WebEraser userscript in non GM_ mode for chrome/safari etc.",environ.GM_getValue);
	environ.chrome=true; environ.unsafeWindow=window;
	environ.old_GM_getValue=environ.GM_getValue;
	environ.GM_getValue=function(a,b) { return localStorage[a]||b; };
	environ.GM_setValue=function(a,b) { localStorage[a]=b; };
	environ.GM_getResourceURL=function(url) {
	    var ext="Dbl"; if (url.endsWith("Orig")) ext=".orig"; else if (url.endsWith("Xsm")) ext="ExSm"; else if (url.endsWith("Trpl")) ext="Trpl";
	    return "https://github.com/SloaneFox/imgstore/raw/master/whiteCurtains" + ext +".jpg";
	};
	//environ.GM_registerMenuCommand=x=>null;
	environ.GM_addStyle=function(cssSheet) { $("head").append("<style>"+cssSheet+"</style>"); };
	environ.uneval=function(x) { return "("+JSON.stringify(x)+")";  }; //Diff is that uneval brackets string and json excludes code only data allowed in json.
	var xhr_queue=[], xhr=new XMLHttpRequest();
	xhr.onload=x=> { //arrow function means this remains window not xhr (as a function would).
	    eval.call(window,xhr.response);
	    if (xhr_queue.length) {  xhr.open('GET', xhr_queue.shift()); xhr.send(); }
	    else main.call(window);
	};
	xhr.onerror=e=> {
	    console.log("XHR Error:",e,xhr);
	    if (xhr_queue.length) xhr.open('GET', xhr_queue.shift()); xhr.send();
	};
	xhr_queue.push("https://code.jquery.com/jquery-3.1.1.js");
	xhr_queue.push("https://github.com/SloaneFox/code/raw/master/GM_registerMenuCommand_Submenu_JS_Module.js");
	xhr_queue.push("https://code.jquery.com/ui/1.12.0/jquery-ui.js");
	
	xhr.open('GET', xhr_queue.shift()); xhr.send();
	
	return true;
    } else 
	return false;

}