WebEraser

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

// ==UserScript==
// @name        WebEraser
// @version     1.4.8
// @namespace   sfswe
// @description Erase parts of any webpage --annoyances, logos, ads, images, etc., permanently with just, Ctrl + Left-Click.
// @license     GPL-3.0-only
// @copyright   2018, slow! (https://openuserjs.org/users/slow!)
// @include     *
// @require     https://code.jquery.com/jquery-3.2.1.js
// @require     https://code.jquery.com/ui/1.12.1/jquery-ui.js
// @require https://greasyfork.org/scripts/375359-gm4-polyfill-1-0-1/code/gm4-polyfill-101.js
// @require https://greasyfork.org/scripts/375360-sfs-utils-0-1-5/code/sfs-utils-015.js
// @resource    whiteCurtains      https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtainsDbl.jpg
// @resource    whiteCurtainsOrig  https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtains.orig.jpg
// @resource    whiteCurtainsXsm   https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtainsExSm.jpg
// @resource    whiteCurtainsTrpl  https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtainsTrpl.jpg
// @icon        https://raw.githubusercontent.com/SloaneFox/imgstore/master/WebEraserIcon.gif
// @run-at      document-start
// @author      Sloane Fox
// @grant       GM.registerMenuCommand
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.deleteValue
// @grant       GM.listValues
// @grant       GM.addStyle
// @grant       GM.getResourceText
// @grant       GM.getResourceUrl
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// ==/UserScript==

//
// History
// updated Nov  2018  v1.4.8 Images and Canvas elements lost functionality, returned.
// updated Oct  2018  v1.4.6 Bug fix re complete deletion of element.  Got around webpage trick of translateZ(0) & css hiding.  Block page change mid-setup.
// updated Jan  2018  v1.3.9   Update for yet to come GM4 and already past backward compatibile GM4 polyfill.  And for Chromium.
// updated Nov  2017  v1.3.8   Added extra GM menu command to enable use of Ctrl-e to manually invoke web erasure on a webpage.
// updated Nov  2017  v1.3.7   Added extra GM menu command in case user accidentally erases entire webpage and is faced a blank with not even a WebEraser menu to allow one to undo accident.
// updated Aug  2017  v1.3.6   Issue with iframe when injected code not called for it due to its late creation same origin.
// updated Jan  2017  v1.3.5   Compatibility working on msedge/safari/opera under tampermonkey.  With ie11 (adGuard method) js engine is too old.  Windows ok with Chrome either native or with tamper.
//                             Safari on windows cant run userscripts.
// updated Jan  2017   v1.3.4  Bug fixes.  Issue with load sequence on Chrome.  Monitoring class changes relating to identifier of element.  Compatibility on msedge/safari/opera under tampermonkey.
// 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).

ttimer("start");

// Globals:
var environ=this, jq_saved, chromert=this.chrome; //window; //this // note 11'17 polyfill acts upon this (sandbox with a member "window") not on window.
if (typeof jQuery!="undefined") jq_saved=jQuery;
var iframe=window!=window.parent, border_width=6;
var win=window,
	host=window.document.location.host,
	pathname=window.document.location.pathname, webpage=host+pathname, website=host;
var askedAlready,last_one_deleted, delcnt=0, 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.
//
// Globs to be initialized asynchronously, see below, init_globs().
//
var page_erasedElems,site_erasedElems,curtain_icon, elems_to_be_hid,curtain_slim_icon,curtain_xslim_icon, 
	curtain_wide_icon, config, ownImageAddr, whitecurtains, whitecurtainsoriginal, whitecurtainstriple;
var ignoreIdsDupped, curtain_cnt=0;
var zaplists,overlay=false;

if (iframe) {
	installEventHandlers();
	return;
}

//if (!environInit()) if (!plat_msedge) $(main.bind(environ)); // In a normal GM environment, main will be called at docready.
var str=GM_registerMenuCommand.toString();
//for (var i=0;i<str.length;i++) console.log(str[i]);
if (!environInit(environ)) if (!plat_msedge) {
	if(/^complete/.test(document.readyState)) main();
	else document.addEventListener("DOMContentLoaded",main.bind(environ)); //main(); addEventListener("load",main.bind(environ))	; // In a normal GM environment, main will be called at docready.
}

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.prefix=function(pfix) { return this.length ? pfix+this : ""+this; };

async function main() {try{
	log=x=>null;  //logger on/off
	ttimer("start of main, state: "+document.readyState);
	log("w/e main GM:",GM, "readyState",document.readyState,"body:",document.body,"iframe",iframe,"jQuery:",window.jQuery&&window.jQuery.fn.jquery,"$",window.$);
	if (!this.chrome) this.chrome=chromert;  
	await init_globs();
	installEventHandlers();
	ensure_jquery_extended(); // may get clobbered by other script loading jQ.
	inner_eraseElements("init");
	var nerased=$(".Web-Eraser-ed").length, delay=5000+300*(2+nerased), forErasure=getHidElemsCmd("count");
	setTimeout(x=> { 
		//ttimer("start of delay phase ");
		log("End of",delay,"delay, checking for inner_eraseElements",page_erasedElems,"or",site_erasedElems);
		if(page_erasedElems || site_erasedElems) inner_eraseElements("delay"); 
		else if ($(".Web-Eraser-ed").length==0 && elems_to_be_hid)
			console.info("WebEraser message: no match for any selectors:",getHidElemsCmd(),"\nWebpage:",webpage);
		var nerased2=$(".Web-Eraser-ed").length;
		nerased=nerased2;
		installEventHandlers("phase2");
		regcmds();
		//ttimer("end delay phase ",document.readyState);
	},delay);
	
	//!!
	// $(window).focus(x=>{
	// 	//$(window).off("focus");
	// 	setTimeout(x=> { //try{
	// 		forErasure=getHidElemsCmd("count");
	// 		var sels=getHidElemsCmd(), nerased=$(".Web-Eraser-ed").length;
	// 		log("WE focus, check for erasure", nerased ,forErasure,"site_erasedElems:",site_erasedElems);
	// 		if (nerased < forErasure) { inner_eraseElements("focus"); }
	// 		//}catch(e){console.error("WebEraser main--focus, error@",e.lineNumber,e);}
	// 	},400);
	// });
	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; } " 
				 +"img.WebEraserCurtain { display: block !important; color:#fff !important; }"
				 +`.CurtainRod {
                     background-color: #bbb;
			    	 background-image: linear-gradient(90deg, rgba(255,255,255,.07) 50%, transparent 50%), linear-gradient(90deg, rgba(255,255,255,.13) 50%, transparent 50%), linear-gradient(90deg, transparent 50%, rgba(255,255,255,.17) 50%), linear-gradient(90deg, transparent 50%, rgba(255,255,255,.19) 50%);
			    	 background-size:  13px, 19px, 17px, 15px;
			       }`
				 +".ui-dialog-buttonpane button {color:black !important;}"
				 +'img[src*="blob:"] { display:block !important; }'
			   ); // A later defined rule has precedence when both rules in effect.
	//setTimeout(inner_eraseElements,1500);
	if (plat_chrome && typeof submenuModule != "undefined") submenuModule.register("WebEraser"); //,"w");
	regcmds();
	setTimeout(reattachTornCurtains,4000);
	gelems=$();
	ttimer("end of main, state: "+document.readyState);
} catch(e){console.log("WebEraser main(), error@",e.lineNumber,e);}} //main()

function handleClick(e,iframe_click) {  try { //called from event handler in page & iframe, and pseudo called from click within iframe.
	if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
	log("CLICK jwin is ","page:",location.href,"iframe?",iframe);
	halter();
	var permrm, target=e.target, frameEl=target.ownerDocument.defaultView.frameElement;
	if(frameEl) target=frameEl;
	prevDef(e);
	if (!iframe_click) { //not pseudo call
		var seltext_len=window.getSelection().toString().length;
		window.status="webEraser, Ctrl-Click, on HTML element:"+target.tagName+" "+seltext_len+", ifame: "+iframe;
		if (seltext_len != 0) { unhalt(); return; }
		if (target.blur) target.blur();
		if (iframe)  {
			window.parent.postMessage( { type:"sfswe-iframe-click", code:0, src:location.toString() },"*"); // msg,origin makes pseudo call back here.
			log("post to parent from window."); //"frameElement:",window.frameElement);
			unhalt(); 
			return false;
		}
	} // endif !iframe_click
	while (/HTMLUnknownElement/.test(target.toString())) target=target.parentNode; //Avoid non HTML tags.
	log("Click reached target",target);
	if ($(target).is(".WebEraserCurtain")) 
		if (e.button==0) {
			let reply=confirm("This will completely erase selected item, continue? \nOn any revisit to webpage you can check in the console for such erasures.");
			if (reply) openCurtains("zap",$(target).siblings("img").addBack());
		} else
			eraseElementsCmd();
	else if (!askedAlready) {
		if ($("body").is(target)) {
			if (!confirm("WebEraser.  You clicked on the main body of the webpage.  Body however, is not removable by ctrl-click, try ctrl-clicking on an image or other item on the webpage.  Hit CANCEL to open erasure window."))
				eraseElementsCmd();
			unhalt();
			return;
		}
		halter.dialog=true;
		inner_eraseElements();
		var prom=checkIfPermanentRemoval(target);
		prom.then(function(confirm_val){     // then takes a function with 2 params, the arg of resolve func call and of reject.
			log("then in checkIfPermanentRemoval, permrm:",permrm,"confirm_val",confirm_val);
			var [permrm,item_sel]=confirm_val;
			halter.dialog=false;
			unhalt();
			if (permrm==Infinity)      { // temp delete
				askedAlready=true; 
				alert("You hit TEMP, item deleted.  Ctrl-click from now until reload merely removes elements temporarily (esc to undo). ");
				last_one_deleted=$(item_sel);
				last_one_deleted.replaceWith("<placeholder delcnt="+(++delcnt)+">");
				console.log("install escape catch");
				escapeCatch(function(){ 
					$("placeholder").replaceWith(last_one_deleted); },"perm");
			}
			log("checkIfPermanentRemoval, permrm",permrm);
			//			if (permrm!=undefined)   inner_eraseElements("click");         //undefined==>escape (cancel)
			if (permrm!=false)       inner_eraseElements("click");           //undefined==>escape (cancel)
		});
		prom.catch(function (a){log("caught in checkIfPermanentRemoval",a); unhalt();halter.dialog=false;});
	} // endif !askedAlready
	else { 
		last_one_deleted=$(target);  //temp delete
		$("placeholder").remove();
		last_one_deleted.replaceWith("<placeholder delcnt="+(++delcnt)+">");
		console.log("set last_one_deleted:",last_one_deleted, "placeholder",$("<placeholder>"));
	} 
	
	if(!halter.dialog) { unhalt();}
	return false;

	function halter() {	window.addEventListener("beforeunload", handlehalt);}
	function handlehalt(event) {
		event.returnValue="a  message to stay";
		//log("event beforeunload: ",event.returnValue);
		return event.returnValue;
	};
	function unhalt(){ 	window.removeEventListener("beforeunload",handlehalt,false);}
} catch(e) { console.log("Click handling error:"+(e.lineNumber),e,e.stack);unhalt(); }}  //handleClick() 

function sprompt(tex,initv,cancel_btn="Cancel",ok_btn="OK",extra_btn){ // 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,extra_btn);
	});
	p.dialog=dialog;
	return p;
}
function sconfirm(msg,cancelbtnText,okbtnText,extrabtnText) { return sprompt(msg,undefined,cancelbtnText,okbtnText,extrabtnText); }
function salert(msg) { return sprompt(msg,undefined,-1,"OK"); }

//Resolution of promise returned is cancel:false, OK: true, extrabtn: Infinity;

function sprompt_inner(pretext,initval,resolve,reject,cancelbtnText,okbtnText,extrabtnText) {try{ // "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);
	try{content.resizable();}catch(e) {log("spromtinner(), err",e.lineNumber,e);}
	var sp1=$(document).scrollTop();
	var dfunc=content.dialog.bind(content);
	var dialog=content.dialog({
		modal: true, width:"auto", position: { my: "center", at: "center", of: unsafeWindow }, // Greater percent further to top.// Position is almost default anyway, difference is use of unsafeWindow due to strange error during prompt in jq in opera violentmonkey
		close: function(e) { dialog.off("keydown"); $(document).scrollTop(sp1); if (e.key=="Escape") reject("Escape");}
	}).parent();
	var 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;}
	};
	if(extrabtnText) buttons[extrabtnText]=function(e) { if (confirm_prompt) resolve(Infinity); else resolve([Infinity,$(this).find("input,textarea").val() || ""]); dfunc("close"); return false;};
	content.dialog("option","buttons",buttons);
	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();
	// var maxH=innerHeight - (content.offset().top-$(window).scrollTop()) - 100;
	// content.css({"overflow-x":"hidden","max-height":maxH}); //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
}catch(e) {log("hlightAndsetsel(), err",e.lineNumber,e);}}

function checkIfPermanentRemoval(target) {   // called from click handler.
	var sconfirm_promise, checkif_resolve, checkif_reject;
		checkif_promise=new Promise((resolve,reject)=>{
			checkif_resolve=resolve;checkif_reject=reject;
			var parent=target.parentNode, index=0;
			var msg="Permanently erase selected element(s) from website &mdash; now seen on page red bordered and blinking?  In addition you may 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);
			sconfirm_promise=sconfirm(msg,"Cancel","OK","Temp"); ////////////////
			var dialog=sconfirm_promise.dialog;
			var buttonpane=dialog.find(".ui-dialog-buttonpane");
			buttonpane.append("<div><input id=sfswe-checkbox7 type=checkbox style='vertical-align:middle'>"+"<label style='display:inline'>&nbsp;&nbsp;Remove just from this page (not entire website).</label></div>");
			buttonpane.append("<br><div style='margin-top:-10px;'><input id=sfswe-checkbox6 type=checkbox style='vertical-align:middle'>"	+"<label style='display:inline;'>&nbsp;&nbsp;Completely delete element.</label></div>");
			dialog.find("a").click(e=>{
				log("click on a in OK");
				dialog.trigger($.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",""); });
			}); //
			hlightAndsetsel(target); // Also sets input value to selector!
			setTimeout(function(){dialog[0].scrollIntoView();},100);
		});//new Promise()
	close_of_prompt(sconfirm_promise, checkif_resolve, checkif_reject);
	log("Got back from close_of_prompt, close_of_prompt:",sconfirm_promise);
	return checkif_promise;
}//end checkIfPermanentRemoval()

function close_of_prompt(sconfirm_promise, checkif_resolve, checkif_reject) {
	var nested_confirm, first_reply, complete_rm, reply_sel;
	sconfirm_promise.catch(function(reply){
		hlightAndsetsel(0,"off","restore");
		log("caught confirm prom");
		checkif_reject("caught");
	});
	nested_confirm=sconfirm_promise.then(function(reply){                 ///////////////////////////////////////
		$(document).off('keypress');
		$(":data(pewiden-trace)").data("pewiden-trace",""); // remove trace
		var complete_rm=$("#sfswe-checkbox6:checked").length!=0;
		var webpage_only=$("#sfswe-checkbox7:checked").length!=0;
		log("complete_rm?",complete_rm, "reply:",reply);
		if (reply!=false) reply_sel=$("#sfswe-seledip").val().trim(); 
		if(reply!=true) { 
			hlightAndsetsel(0,"off","restore"); 
			//if(reply==Infinity) checkif_resolve([reply,$(reply_sel).detach]); //$(reply_sel).hide(); // temp delete
			checkif_resolve([reply,reply_sel]); 
			return; 
		};
		sconfirm_promise.data=[reply_sel,complete_rm]; // use ES6 await?
		if (reply_sel)  {
			let ancErased=$(reply_sel).closest(".Web-Eraser-ed");  //.closest, includes current.
			if (hidElementsListCmd("isthere?", reply_sel) || (ancErased.length && getHidElemsCmd("match el",ancErased))){ 
				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_sel+"\n\n   Ancestor:"+nodeInfo(ancErased)); 
				console.info("Already erasing",ancErased,nodeInfo(ancErased),".  Your selector",reply_sel);
				hlightAndsetsel(0,"off","restore");
				checkif_reject("Ancestor Already");
				return; 
			}
			if (!webpage_only)
				hidElementsListCmd("add",reply_sel+" site");
			else
				hidElementsListCmd("add",reply_sel);    // btn1 -> null, btn2 -> "<string>" null==undefined
			if (hidElementsListCmd("rm", $(reply_sel).find(".Web-Eraser-ed"))) console.info("Removed child selectors of",reply_sel);
			hlightAndsetsel(0,"off","restore");
			log("reply true, complete_rm@",complete_rm,"add sel:",reply_sel);
			if (complete_rm) zaplists.add(reply_sel);
			checkif_resolve([true,reply_sel]);
			
		} else {  // empty reply.
			hlightAndsetsel(0,"off","restore"); 
			checkif_reject("empty");
		}
	});// sconfirm_promise.then        /////////////////////////////////////////////
}

function eraseElementsCmd() { 
	// Called from GM script command menu and from clickable within ctrl-click  prompt.
	// 
	var erasedElems, no_sels;
	erasedElems=getHidElemsCmd("with site");
	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; eg, 'iframe' will hide all iframes (often ads). " 
			+"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||whitecurtains;
							curtain_slim_icon=reply2||whitecurtainsoriginal;
							curtain_wide_icon=reply2||whitecurtainstriple;
							$(".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]
			log("Got reply array:",reply);
			if(reply.length) {
				page_erasedElems=[]; site_erasedElems=[]; 
				$(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);
				});
				site_erasedElems=site_erasedElems.toString();
				page_erasedElems=page_erasedElems.toString();
			}
			try{$(reply);} catch(e){alert("Bad selector given."); throw(e);}
			setValue("config",config);
			setValue(website+":erasedElems",site_erasedElems);
			setValue(webpage+":erasedElems",page_erasedElems);
			zaplists.update();
			//log("end awaaits of setvalue");
			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");
			});
			$(".CurtainRod").remove();
			//log("set tout")
			setTimeout(inner_eraseElements,1000,"prompt"); //'cos openCurtains takes time
			//inner_eraseElements("fromPrompt");
		}
	});//prompt_promise.then()
	var keep_layout=config.keepLayout;
	var dialog=prompt_promise.dialog;
	dialog.find(".ui-dialog-buttonpane").prepend(
		"<div class=sfswe-ticks style='float:left;font-size:10px;'>" //width:78%;
	+"<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>Also hide curtains.</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({
		//cmtd out 8/17	"-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=getHidElemsCmd(), 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");
	log("inner_eraseElements, erasedElems_ar",erasedElems_ar, "from:",from);
	erasedElems_ar.forEach(function(sel,i){
		erasedElems=$(sel); //Array.from(document.querySelectorAll(sel)); //$(sel), jQ cannot find duplicate ids.
		erasedElems.each(function() {
			log("inner_eraseElements Found for sel",sel," el: ",this);
			var eld=this,el=$(eld); // 40msecs per 'each' loop.
			if(/delay|focus/.test(from)) { // skip curtains already closed.
				var crod=jQuery.data(eld,"rod-el"); //el.prev()[0];
				if (crod && /^sfswediv/i.test(crod[0].tagName)) {el.addClass("Web-Eraser-ed");return;}
			}
			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 && sel) nomatch.push(sel);
	}); //forEach()
	if (iframe || count==0) 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 the following selectors at",webpage+":");
		nomatch.forEach(nom=>console.info("\t",nom));
	}
	var ieemsg="Userscript WebEraser has 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 rod=jQuery.data(this,"rod-el");
		var is_an_overlay=rod && rod.hasClass("sfswe-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."
			  : "" );
	});
	ieemsg+="(phase:"+from+")";
	count=0;
	console.info(ieemsg);
	bodymsg(ieemsg.replace(/(.*\n){2}/,"")+"  Whence: "+from+".","init");
}

function closeCurtains(el, noAnimKeepLayout, finishedCB=x=>x) {   //called from inner_eraseElements()
	//log("closeCurtains1, el:",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, wediv=el.children("sfswediv"), curtainRod, lrcurtains;
	var old_curtained=wediv.length ? jQuery.data(wediv[0],"covered-el") : null;
	if ( ! old_curtained || ! old_curtained.is(el))
		[curtainRod,lrcurtains]=createCurtains(el,noAnimKeepLayout);
	else { curtainRod=el.children("sfswediv"), 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",15,"%"],1000,2);
		manimate(lrcurtains,["width",51,"%",1000],1000,5,function(){ ///////////////////////Animation
			log("Anim end",lrcurtains,"Width of left curtain:",lrcurtains.css("width"));
			lrcurtains.css("width","51%");
			curtainRod.css("visibility","visible");
			el=jQuery.data(this.parentNode, "covered-el")||$();
			if (!keep_layout || curtainRod.hasClass("sfswe-overlay")||onzaplist.zap) {
				//console.log("Anim end, fade curtains");
				el.add(curtainRod).delay(200).fadeOut(       // $.add here prepends.
					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) {
				//console.log("Anim end, fade out 2");
				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) {
				//console.log("Anim end, call CB");
				finishedCB();
			}
		}); //animate()
	}
	return false;
} //closeCurtains()

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.
				hlightAndsetsel(0,"off",null,"mere_highlight"); 
				hlightAndsetsel($(cval),null,null,"mere_highlight"); }
			else hlightAndsetsel(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);} };

// GM_registerMenuCommand("Temporary web deleter, ctrl-click",function(){
//     window.addEventListener("mousedown",function(e){
// 	if(e.ctrlKey) {
// 	    if (e.preventDefault) { e.preventDefault(); e.stopPropagation(); }
// 	    e.target.style.setProperty("display","none","important");
// 	}
//     },true);
// });
//thousand's comma, call Number.toLocaleString()
//if (iframe) console.log=x=>null;       //logger(); // Logs from doc start.

async function init_globs() { // all globs asynchronously set.
	log("init_globs");
	page_erasedElems=(await getValue(webpage+":erasedElems","")).trim();
	//loader Failed here.
	site_erasedElems=(await getValue(website+":erasedElems","")).trim();
	elems_to_be_hid=getHidElemsCmd();
	zaplists=new zaplist_composite(); 
	await zaplists.update(); //depends on site/page_erasedElems being read first.

	ownImageAddr=await getValue("ownImageAddr","");
	whitecurtains=await GM.getResourceUrl("whiteCurtains");
	// Ensure visit to https matches getResourceUrl use of https or address as given in header w/wo ssl!
	whitecurtainsoriginal=await GM.getResourceUrl("whiteCurtainsOrig");
	whitecurtainstriple=await GM.getResourceUrl("whiteCurtainsTrpl");
	curtain_icon=ownImageAddr|| whitecurtains;
	curtain_slim_icon=await getValue("ownImageAddr","")||whitecurtainsoriginal;
	curtain_xslim_icon=await getValue("ownImageAddr","")||await GM.getResourceUrl("whiteCurtainsXsm");
	curtain_wide_icon=await getValue("ownImageAddr","")||whitecurtainstriple;
	config=await getValue("config",{keepLayout:"checked",monitor:{}});
	if (!config.monitor) config.monitor={};
	if(!jQuery.ui)  { // GM4 loader problem. Another userscript is loaded that has jq but no jQuery-UI will clobber $.ui here.
		$=jQuery=jq_saved;  // saved from when prior to async branch.
		console.log("GM4 FAILure, jq.ui clobbered, patching up.",jQuery.ui);
	}
}

function installEventHandlers(phase2) {
	if(!phase2) {
		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);
		if(iframe) window.installedEHs=true;
		//console.log("installed handlers, window.mousedown, at:",location.href,".  In iframe?",iframe);
	}
	else {
		$("iframe").each(function(){
			var fwin=this.contentWindow; try{ //perhaps permission error due to iframe origin.
				if (!fwin.installedEHs) {
					fwin.addEventListener.call(fwin,"mousedown",handleClick,true);
					fwin.addEventListener.call(fwin,"message", postMessageHandler2,false);
					//despite use of call(), event is still triggered in this context not in iframe's hence use of frameElement.
				}} catch(e){};
		});
	}
}

function prevDef(e) { if (e.preventDefault) {	e.preventDefault();   e.stopPropagation(); e.stopImmediatePropagation();  }
					} //else console.log("No preventDefault for event",e);	}					       

function postMessageHandler(e){ //reads postMessage().
	if ( ! e.data.type || e.data.type!="sfswe-iframe-click") return;
	//log("Handle a PostMessage, iframe:",iframe, "code:",e.data.code,"data",e.data,[e]);
	if (iframe) {
		window.parent.postMessage({type:"sfswe-iframe-click",code:++e.data.code},"*");
		return;
	}
	var iframeEl; 
	$("iframe, embed").each((i,el)=>{ 
		// log("cmp, el.cont",el.contentWindow," e.source",e.source," e.origin",e.origin,typeof e.origin);
		// log("getsvg",el.getSVGDocument,el);
		// log("src",e.data.src,"==",el.src);          //xss perm denied, e.source.toString(),
		if (el.contentWindow==e.source ||e.data.src==el.src) { iframeEl=$(el); return false; }
	});

	// var iframeEl=$("iframe, embed").filter(function(){ 
	// 	log("cmp, this.cont",this.contentWindow," e.src",e.source,"res:",this.contentWindow==e.source);
	// 	return this.contentWindow==e.source; 
	// });
	handleClick({target:iframeEl[0],ctrlKey:true},"iframe_click");
}

function getSelectorWithNearestId(target,exclude_classes) { // need extended jquery for :regexp 
	ensure_jquery_extended(); 
	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 getHidElemsCmd(cmd,el){
	var els, pels=page_erasedElems, sels=site_erasedElems;
	//console.log("Got sels as:",sels);
	switch(cmd) {
		
	case "match el":  return el.is($(getHidElemsCmd()));
	case "count":     return getHidElemsCmd().split(/,/).reduce(function(prev_res,sel){return prev_res+$(sel).length;},0);
	case "with site": sels=sels.replace(/,/g," site,")+(sels ? " site" : ""); // see reverse of this in hidElementsListCmd() and  eraseElementsCmd().
	default:          return pels + (sels && pels ? "," : "") + sels;	// if (justpels_ar) return pels.split(","); //webpage elements.
	}
}

function hidElementsListCmd(cmd,str,str2) {
	//console.log("hidElementsListCmd, cmd:",cmd, "str:",str,"str2:",str2, "HidElems:",getHidElemsCmd());
	var  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 $) { 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 getHidElemsCmd().split(/,/).includes(str);
		//return getHidElemsCmd().split(/,/).reduce((prev_res,next)=>prev_res||next==str,false);
	}
	log("hidElementsListCmd",cmd,str,str2,"SetValues: ",site_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 hlightAndsetsel(elem, off, restore, mere_highlight) {try{ //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("W/e 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"});
			if (plat_msedge) // catch of escape not working on msedge.
				if ($(".ui-dialog").css("display")=="none") 	hlightAndsetsel(0,"off","restore"); 
		},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); 
	}
}catch(e) {console.error("hlightAndsetsel(), err",e.lineNumber,e);}}


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("body")) {
		blinkBorders(gelem); //blink double indicates top of hierarchy.
		return;
	}
	hlightAndsetsel(0,"off");
	hlightAndsetsel(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 hlightAndsetsel()
	if(trace.length==0) trace=gelem.find(">:only-child");
	if (trace.length==0) {
		blinkBorders(gelem);
		return;
	}
	hlightAndsetsel(0,"off");
	hlightAndsetsel(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 to invoke widen/narrow with 'w' and 'n' keys resp.\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 ensure_jquery_extended() { 
	if ($.expr[":"].regexp) return; // already extended, not yet clobbered.
	$.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[':'], {       // Check it's there with $.expr[":"].regexp.toString()
		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);
		}}); //$.extend()     //usage eg: $(“div:regexp(className,promo$)”);

	(function($){ 
		$.event.special.destroyed = {
			remove: function(o) {
				if (o.handler) {
					o.handler();
				}
			}
		}; })($); //Usage: $("#anid").bind('destroyed', function() {// do stuff}) // only for is jQ  removed el.
} //extend_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() 
		.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")||"").trim(); // Don't use this.className (animated string issue)
			cl=cl.split(/\s+/).join(".").prefix("."); 
			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){logError("Can't get selector for "+desc,e); }}  //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.
	log("end markForTheCurtains, classes:",eld.className);
}

function reattachTornCurtains(curtains=$(".CurtainRod")) {try{
	var torn=false;
	curtains.each(function(){
		var that=$(this), el=jQuery.data(this,"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();
} catch(e){console.error("WebEraser reattachTornCurtains(), error@",e.lineNumber,e);}}

function measureForCurtains(curtains=$(".CurtainRod")) {
	curtains.each(function(){
		//console.log("measureForCurtains for el(data-covered) as:",jQuery.data(this,"covered-el"));
		var that=$(this), el=jQuery.data(this,"covered-el");          // $.data seems to lose its info when another userscript is also running, jQuery.data works.
		if(!el) {console.error("noel");el=$();}

		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);
		//that.css({height:h,width:w});
		//this.style.setProperty("width",w+"px","important");

		if(!that.hasClass("outie")) {
			that.css({left:0,top:0});
			this.style.setProperty("width","100%","important");
			this.style.setProperty("height","100%","important");
		}else {
			var offset=moffset(el);
			that.css(offset).css({height:h,width:w});
			this.style.setProperty("width",w+"px","important");
		}

		
	});};

function bodymsg(str,init) { // Append string to a <pre> that is initially appended to body.
	if ($("#sfswe-div-logger").length==0) $("body").prepend("<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(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=getHidElemsCmd(),
		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.children("sfswediv").css("display","none");
				measureForCurtains();
			} else if (currval.display!="none" && (oldval.display=="none" || oldval.display==undefined )) {
				bodymsg("change-display:"+target.attr("selmatch-sfswe"));
				target.children("sfswediv").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"));
				$(".CurtainRod[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"
	log("openCurtains",zap_or_keep,"curtains:",curtains);
	setTimeout(function() {
		curtains.each(function() { $(this).parent().css("visibility","hidden");});
		manimate(curtains,["width",0,"%"],3500,8,function() {
			var that=$(this), erased_el=jQuery.data(this.parentNode,"covered-el"); 
			var sel=erased_el.attr("selmatch-sfswe");
			bodymsg("opened curtains for sel:"+sel+", cc:"+erased_el.attr("cc"));
			switch(zap_or_keep[0]) { // z: zap from layout, k: keep layout, t temporarily rm curtains, a: alt rm erasure
			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);
	return false;
}

// Outline overview of layout:
//
// <DIV id=xyz class=Web-Eraser-ed selmatch-sfswe="DIV#xyz"> // target el, for covering.  el.children("sfswediv") has data covered-el to here.
//   <sfswediv class=CurtainRod cc=n data.coveredEl=divtarget>             
// 	    <img class=webEraserCurtain sfswe-left>                        
// 	    <img> class=webEraserCurtain sfswe-right>
//   </sfswediv> 
// </DIV> 

// 

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");
	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) {
	if(!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%;visibility:visible;'>");
	lcurtain.attr("src",lsrc);
	setTimeout(()=>{
		if (lcurtain[0].complete||plat_chrome) return;
		lcurtain[0].src="https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtains.orig.jpg";
		rcurtain[0].src="https://raw.githubusercontent.com/SloaneFox/imgstore/master/whiteCurtains.orig.jpg";
	},500);
	var rcurtain=$("<img class='WebEraserCurtain sfswe-right' style='right:0;position:absolute;height:100%;visibility:visible;' src="+rsrc+"></img>"), 
		curtainRod=$("<sfswediv tabindex=0 class=CurtainRod cc="+(++curtain_cnt)+" style='z-index:2147483640; position:absolute; display:block; opacity:0.94;visibility:hidden'></sfswediv>"),//background-color:#888; !!overflow:hidden; rm'ed //inline is default here, 'd take full width of parent.
		lrcurtains=lcurtain.add(rcurtain), sel=el.attr("selmatch-sfswe");
	//Absolute is relative to nearest non-statically positioned ancestor, this is returned from elem.offsetParent.
	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+", webpage is:"+webpage+".";

	lrcurtains.contextmenu(e=>(eraseElementsCmd(),false));
	lrcurtains.click(function({ctrlKey:ctrl,shiftKey:shift,altKey:alt,target:target}) {
		var that=$(this),lrcurtains=that.add(that.siblings());
		log("lrcurtains.click alt",alt,"lrcurtains:",lrcurtains,"this:",this);
		if (!(alt||shift)) return;
		if (ctrl&&shift) alert("Curtained target is,"+target,"lrcurtains:",lrcurtains,"this",this);
		else if (shift) openCurtains("keep_layout",lrcurtains);
		else if (alt) openCurtains("azap",lrcurtains);
		else if (ctrl&&alt) that.parent().focus();
		return false;
	});
	lrcurtains.dblclick(e=>openCurtains("tzap",lrcurtains));
	curtainRod.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});
	//curtainRod.css({height:h,width:w}); //.css(pos);

	curtainRod[0].style.setProperty("float","none","important");
	curtainRod[0].style.setProperty("width",w+"px","important");
	jQuery.data(curtainRod[0],"coveredEl",el);
	jQuery.data(el[0],"rodEl",curtainRod);
	//log("data covered-el",jQuery.data(curtainRod[0],"covered-el"),"for rod",curtainRod[0]);
	//log("\nAnd rod-el is ",jQuery.data(el[0],"rod-el"),"for el",el[0]);
	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 element, chosen for erasure, is an Overlay (>2/3 covered, "+portions+"%, "+h+"x"+w+"): ", sel, el);}}
	assertZ(el);      
	if(!el.is("iframe,img,canvas")) { 
		curtainRod.css({height:"100%",width:"100%",left:0,top:0});
		el.prepend(curtainRod);        ////////////////////
		curtainRod.parent().css("position","relative"); // ensure also that left and top are 0.
	}
	else { 
		curtainRod.addClass("outie"); 
		let pos=moffset(el);
		log("OUTIE pos",pos,h,w, "$pos:",el.position());
		curtainRod.css({height:h,width:w}).css(pos); //	curtainRod.css({height:"100%",width:"100%",left:0,top:0});
		el.wrap("<div class=sfswe-contner>");
		el.before(curtainRod); 
	}
	return [curtainRod,lrcurtains];
}

function moveRod(e) {
	if (e.key=="w"||e.key=="n") {
		let  newel, rod=$(this), el=jQuery.data(this,"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);
		jQuery.data(this,"covererEl",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; 
	$(".CurtainRod").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.  4 objs.  Those on zaplist are for complete erasure, but may keep layout.
	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() { var p=setValue(fullkey,list);
							  if (!list.length && GM.deleteValue) p=GM.deleteValue(fullkey);
							  //log("saved zaplist fullkey",fullkey,", list:",list,"getval");
							  return p;
							};
	var readlist=function() { return getValue(fullkey,[]); }; 
	var list;   //console.log("zap inited:",key,keytype);
	
	this.add=async function(str,kl) {
		log("zaplist add, str:",str,"in kl:",kl,"this.fullkey:",fullkey);
		if (!!kl != !!keytype) return;
		list.push(str);
		if((await getValue(key+":erasedElems","")).split(/,/).includes(str)) {
			await savelist();
		} else { log("pop goes the attempt");list.pop();}
	};
	this.contains=function(jqobjOrStr){
		//log("zap check if list contains obj:",jqobjOrStr,"within its \nlist:",list,"FUll key",fullkey);
		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=async function() { // If sels removed from main list also remove from zaplists.
		if(!list) list=await readlist();
		//log("zaplist.update key:",fullkey,"read list:",list);
		//if (list.length==0) return;
		var strs_ar=getHidElemsCmd().split(/,/);
		list=list.filter(function (lel) {
			return strs_ar.includes(lel);
		});
		savelist();
	};
	this.toString=()=>"[object zaplist]";
}

function moffset(elem, eld=elem[0]) { try{
	if (  elem.find("*").addBack().filter(function(){
		return $(this).css("position").includes("fixed");
	}).length )	                                               //    if (elem.css("position").includes("fixed")) 
		return Object.assign(elem.position(),{position:"fixed"});
	var dominPar=elem.offsetParent()[0]; // gets closest element that is positioned (ie, non static);
	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 };
	}
}    catch(e){ console.log("Error moffset",e); }}	      

function assertZ(el){
	var dominPar=el.offsetParent();
	//	var tnames=["transform","-webkit-transform","-webkit-perspective"];
	var tnames=["transform","perspective"];             // jquery adds vendor suffixes, eg -webkit-
	el.parentsUntil(dominPar).addBack().each(function(){
		var that=$(this), tforms=that.css(tnames),tf={}; 
		//log("assertZ dominpar:",dominPar,"tforms:",tforms);
		if(Object.values(tforms).some(x=>!/none/.test(x))) {
			tnames.forEach(name=>tf[name]="none");
			that.css(tf);that.addClass("assertedZ");
		}
		//log("assertz changed css",tforms,"\n",tforms,"now it is: ",that.css(tnames));
	});
}

//
// 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,delay],interval,noOf_subintervals,CB) { // CB is invoked once, at end.  $.animate max-ed out cpu for 30 secs or so.
	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);
	//console.log("manimate() targets:",objs," requestAnimationFrame:",css_attr,"currval:",objs.css(css_attr),target_val,interval,noOf_subintervals,CB,objs,"plotvals:",plotvals);

	subinterval+=random(-subinterval/random_element,subinterval/random_element);  /// Random element +/- 1/random_element.
	if (delay) setTimeout(x=>i=setInterval(eppursimuove,subinterval,objs),delay);
	else i=setInterval(eppursimuove,subinterval,objs);
	function eppursimuove(that,b,c) {try{
		requestAnimationFrame(tstamp=>objs.css(css_attr,plotvals[cnt]+suffix));
		if (++cnt==noOf_subintervals) {     
			clearInterval(i);  
			CB && CB.call(that[1]);
		}
	} catch(e){log("WebEraser eppursimuove(), error@",Elineno(e),e);}}
} //manimate()

async function regcmds(){
	var reg_args;
	if(!regcmds.done) {
		reg_args=["Erase Web Elements ["+(elems_to_be_hid?"some erased":"none erased")+"]", eraseElementsCmd,"","", "E"];
		if(!GM_registerMenuCommand(...reg_args))  // from GM4_registerMenuCommand_Submenu_JS_Module, if there, undefined, else from gm4-polyfill which returns the menuitem DOM object.
			GM.registerMenuCommand(...reg_args);  // from gm4-polyfill.  It sets body contextmenu style menu.
		var ctrl_e_ON=await getValue("eraseElems_ctrlE",false);
		if(ctrl_e_ON) $(window).keypress(function(e) {
			if (!e.ctrlKey || e.key!="e") return;
			setTimeout(function(){salert("Invoking web erasure function.");},300);
			inner_eraseElements("fromCtrlE");
		});
		
		reg_args=["Set ctrl-e to invoke WebEraser now"+(ctrl_e_ON ? "[on]" : "[off]"), function(){
			ctrl_e_ON^=true;
			setValue("eraseElems_ctrlE",ctrl_e_ON);
			alert("Ctrl-e function is now "+(ctrl_e_ON?"enabled":"disabled")+".  When pages load the erase function is invoked.  However, if the webpage is unusual "
				+"it may delay this erasure, ctrl-E can be typed to invoke the erasure at any time when "
				+"ctrl-e function is enabled.  Select again from menu to toggle");
		},"","", ""];
		if(!GM_registerMenuCommand(...reg_args))
			GM.registerMenuCommand(...reg_args);
		regcmds.done=1;
	}// endif ! regcmds.done
	if (regcmds.done==2 || $(".Web-Eraser-ed").length == 0) return; 
	reg_args=["Clear All WebErasures here on page & site; reloads page.",async function(){
		await deleteValue(website+":erasedElems");
		await deleteValue(webpage+":erasedElems");
		location=location;
	}];
	if(!GM_registerMenuCommand(...reg_args))
		GM.registerMenuCommand(...reg_args);
	regcmds.done=2;
} // regcmds()

function setValue(n,v) { 
	if (!v && GM.deleteValue) return GM.deleteValue(n);
	else return GM.setValue(n,JSON.stringify(v)); 
}
async function getValue(n,v) { var r1,res=await GM.getValue(n,JSON.stringify(v)); try {
	r1=JSON.parse(res); return r1; } catch(e) { console.log("Error in parse of res:"+res+".Value:"+v+".  Error:",e); return 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
	// Use with eg, time("start"); ... time("end"); // Each call to time()  for from start and from previous call to time().
	//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 logger2() {
	var originalLogger = console.log;
	logger2.log=originalLogger;
	console.log = function () {
		//alert(Array.from(arguments));
		var roll="";
		for (var i=0;i<arguments.length;i++)
			roll+=arguments[i]+" ";
		document.body.innerHTML+=roll+"<br>";
		//originalLogger.apply(this, arguments);
	};
	// var originalInfo = console.info;
	// logger2.info=originalInfo;
	// console.info = function () {
	// 	//alert(Array.from(arguments));
	// 	document.body.innerHTML+=Array.from(arguments);
	// 	originalInfo.apply(this, arguments);
	// };
	// var originalError = console.error;
	// logger2.error=originalError;
	// console.error = function () {
	// 	//alert(Array.from(arguments));
	// 	document.body.innerHTML+=Array.from(arguments);
	// 	originalError.apply(this, arguments);
	// };
}

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;
}

function ttimer(stage) { 
	return; //!! for profiling only.
	if(window==window.parent) {
		console.time("----from "+stage);
		if(ttimer.last_stage) console.timeEnd(ttimer.last_stage); // print to console: "tTimer[n]: [num]ms"

		if(ttimer.last_stage) console.log("\t----to "+stage);
		ttimer.last_stage="----from "+stage;
	}
}

function escapeCatch(cbfunc,perm) { // Usage: call first time to install listener & add a callback for keydown of escape key.  Optionally then call many times adding callback functions.
	var that=escapeCatch;
	if(!that.flist || that.flist.length==0) {
		that.flist=[cbfunc];
		window.addEventListener("keydown", subfunc, true);
		function subfunc(e) { 
			if (e.which == 27)  {
				console.log("escape",perm,"this is:",this);
				that.flist.forEach(func=>func());
				if(perm) return;
				window.removeEventListener("keydown", subfunc, true); 
				that.flist=[];
			}
		};
	} else
		if(cbfunc) { that.flist.push(cbfunc); return; }
}


function summarize(longstr, max=160)  {
	longstr=longstr.toString();
	if (longstr.length<=max) return longstr;
	max=(max-3)/2;
	var begin=longstr.substr(0,max);
	var end=longstr.substr(longstr.length-max,max);
	return begin+" ...●●●●... "+end;
}

function jqueryui_dialog_css() {
	return ".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 {  width:94%; 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;  }"
			".sfswe-content :-moz-any(div,input) { font-size:13px;padding:0px;margin: 0;color:#333; opacity:1;  }" //background: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 environInit(environ) { // returns false if GM environment is there, otherwise it calls main when ready and immediately returns true.
	environ.plat_chrome=false; environ.plat_msedge=false;        //chrome standalone, ie, not under tamper in chrome.	// environ.plat_msedge=/Edge[\d./]+$/i.test(navigator.userAgent);
	if (/Chrome/.test(navigator.userAgent)) environ.plat_chrome=true;
	environ.plat_mac = /^Mac/.test(navigator.platform);

	try { environ.nonGMmode= (typeof GM == "undefined"); } // || "Barychelidae"!=GM_getValue("arachnoidal","Barychelidae"); }
	catch(e) { environ.nonGMmode=true; }; //eg, chromium stadalone

	//environ.nonjQ = !window.jQuery || parseFloat(jQuery.fn.jquery) < 3.1;
	
	if (nonGMmode){ // chromium bare, ie, w/o tamper.
		console.info("WebEraser userscript in non GM_ mode at "+location.href, "typeof GM:",typeof GM, "nonGMmode",nonGMmode);
		environ.unsafeWindow=window;
		environ.old_GM_getValue=environ.GM_getValue;
		try { localStorage["anothervariable"]=32; }	catch(e) {
			window.nostorage=true;
			if(!iframe) console.error("No local storage, no GM storage, use Tampermonkey to include this script on page:",location.href);
			window.localStorage={};
		}
		//if(!window.nostorage) console.log("Have local storage",localStorage.anothervariable);
		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://raw.githubusercontent.com/SloaneFox/imgstore/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).
			//console.log(xhr.responseURL,"onload to eval in window, jQuery in window? ",!!window.jQuery,!!window.$,!!this.jQuery);
			var synop=(xhr.response||"").substr(0,40);
			try {
				eval.call(window,xhr.response); } catch(e) {  console.error("Can't eval Error:"+e,".  Response:",xhr.response?xhr.response.substr(0,60)+"[60chars]":"No response text",x,xhr,", Queue:",xhr_queue); }
			if (xhr_queue.length) {  xhr.open('GET', xhr_queue.shift()); xhr.send(); }
			else if (!iframe) main.call(window); //////////////////
			
		};
		xhr.onerror=e=> {
			console.log("W/e XHR Error: "+e,", E:",e,"XHR:",xhr,"After error queue:",xhr_queue);
			if (xhr_queue.length) xhr.open('GET', xhr_queue.shift()); xhr.send();
		};
		var jq_versions_prior={ 
			core: parseFloat(environ.$ && environ.$.fn && environ.$.fn.jquery) || 0,
			ui: parseFloat(environ.$ && environ.$.ui && environ.$.ui.version) || 0 
		};
		
		if(jq_versions_prior.core < 1.7)
			xhr_queue.push("https://code.jquery.com/jquery-1.7.2.js");
		if(jq_versions_prior.ui < 1.12)
			xhr_queue.push("https://code.jquery.com/ui/1.12.1/jquery-ui.js");
		xhr_queue.push("https://raw.githubusercontent.com/SloaneFox/code/master/gm4-polyfill-1.0.1.js");
		xhr_queue.push("https://raw.githubusercontent.com/SloaneFox/code/master/gm-popup-menus-1.3.7.js");
		xhr_queue.push("https://raw.githubusercontent.com/SloaneFox/code/master/sfs-utils-0.1.5.js");
		xhr.open('GET', xhr_queue.shift()); xhr.send();
		return true; 
	} else return false;              //if (nonGM || nonJQ)
}
ttimer("end globs setup");