slow! / WebEraser

// ==UserScript==
// @name        WebEraser
// @version     1.4.1
// @updateURL   https://openuserjs.org/meta/slow!/WebEraser.meta.js
// @namespace   sfswe
// @description Erase parts of any webpage --annoyances, logos, ads, images, etc., permanently with just, Ctrl + Left-Click.
// @license     GPL-3.0
// @copyright   2017, 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://raw.githubusercontent.com/SloaneFox/code/master/gm4-polyfill.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.addStyle
// @grant       GM.getResourceText
// @grant       GM.getResourceUrl
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// ==/UserScript==

//
// History
// 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).


// Globals:
var environ=this, chromert=this.chrome; //window; //this // note 11'17 polyfill acts upon this (sandbox with a member "window") not on window.
var iframe=window!=window.parent, border_width=6;
var jq_versions_prior;

var win=window,
	host=window.document.location.host,
	pathname=window.document.location.pathname, webpage=host+pathname, website=host;
var 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.

// Await jquery (GM4 issue or start-at problem)
//(function GM_wait() { if(!this.jQuery) setTimeout(GM_wait,100); else startscript(); })();

startscript();

function startscript() { // used in order to wait for jQuery to be loaded.
	if (iframe) {
		installEventHandlers();
		return;
	}
	
	jq_versions_prior={ core: parseFloat(environ.$ && environ.$.fn && environ.$.fn.jquery) || 0,
						ui: parseFloat(environ.$ && environ.$.ui && environ.$.ui.version) || 0 
					  };
	//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) 
		addEventListener("load",main.bind(environ))	; // In a normal GM environment, main will be called at docready.
	//document.addEventListener("DOMContentLoaded",main);
	//$.ready(main);// no worky // as DOMContentLoaded but even if past it.
	
	// if (plat_msedge) main();
}


// 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=new zaplist_composite(), overlay=false;


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() {
	//console.log("w/e main GM:",GM, "readyState",document.readyState,"body:",document.body, " this is:,",this,"chrome:",this.chrome,"environ is:",environ);
	if (!this.chrome) this.chrome=chromert;
	await init_globs();
	installEventHandlers();
	extend_jquery();
	inner_eraseElements("init");
	var nerased=$(".Web-Eraser-ed").length, delay=5000+300*(2+nerased), forErasure=getHidElemsCmd("count");
	setTimeout(x=> {
		if (nerased < forErasure) { 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;
		//nerased=(nerased-1) + (nerased2-nerased);
		installEventHandlers("phase2");
		regcmds();
		//	if (config.monitor[website]) setTimeout(observeThings,2000*nerased);
	}, delay);
	$(window).focus(x=>{
		//$(window).off("focus");
		setTimeout(x=> {
			forErasure=getHidElemsCmd("count");
			var sels=getHidElemsCmd(), nerased=$(".Web-Eraser-ed").length;
			if (nerased < forErasure) { inner_eraseElements("focus"); }
		},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; } " 
			   ); // 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=$();
} //main()
function typeofObj(unknown_obj){ return ({}).toString.call(unknown_obj); }
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,"in iframe?",iframe);
	if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
	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) return;
	    if (target.blur) target.blur();
		if (iframe)  {
			window.parent.postMessage( { type:"sfswe-iframe-click", code:0 },"*"); // msg,origin makes pseudo call back here.
			return false;
		}
	} // endif !iframe_click
	while (/HTMLUnknownElement/.test(target.toString())) target=target.parentNode; //Avoid non HTML tags.
	if ($(target).is(".WebEraserCurtain")) 
		if (e.button==0) {
			let reply=confirm("This will completely remove selected item, continue?");
			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."+iframe))
				eraseElementsCmd();
			return;
		}
		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 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: 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
		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();
	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
}

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) 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);
			confirm_promise=sconfirm(msg,"Temp","OK");
			var dialog=confirm_promise.dialog;
			var buttonpane=dialog.find(".ui-dialog-buttonpane");
			buttonpane.append("<div><input id=sfswe-checkbox7 type=checkbox style='vertical-align:middle'>"
	+"<label style=''>&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=''>&nbsp;&nbsp;Completely delete element.</label></div>");
			dialog.find("a").click(e=>{
				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(confirm_promise, checkif_resolve);
	return checkif_promise;
}		       //end checkIfPermanentRemoval()

function close_of_prompt(confirm_promise,checkif_resolve) {
	var nested_confirm, first_reply, complete_rm;
	confirm_promise.catch(function(reply){
		hlightAndsetsel(0,"off","restore"); 
	});
	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;
		var webpage_only=$("#sfswe-checkbox7:checked").length!=0;
		if (reply) reply=$("#sfswe-seledip").val().trim(); 
		else { hlightAndsetsel(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 && 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+"\n\n   Ancestor:"+nodeInfo(ancErased)); return; }
			if (!webpage_only)
				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);
		    hlightAndsetsel(0,"off","restore");
		    if (complete_rm) zaplists.add(reply);
		    checkif_resolve(true);
			
			//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==""
			hlightAndsetsel(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);
	// 	hlightAndsetsel(0,"off","restore");
	// 	if (complete_rm) zaplists.add(reply);
	// 	checkif_resolve(true);
	// }); //then()
	// nested_confirm.catch(function() {
	// 	hlightAndsetsel(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=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.  " 
			+"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]
			$(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='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");
	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 the following selectors at",webpage+":");
		nomatch.forEach(nom=>console.info("\t",nom));
	}
	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}/,"")+"  Whence: "+from+".","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",15,"%"],1000,2);
		manimate(lrcurtains,["width",51,"%",1000],1000,5,function(){ ///////////////////////Animation
			//console.log("Anim end",lrcurtains,"Width of left curtain:",lrcurtains.css("width"));
			el=$(this).closest(".WebEraserCurtains").data("covered-el");
			if (!keep_layout || curtainRod.hasClass("sfswe-overlay")||onzaplist.zap) {
				//console.log("Anim end, fade curtains");
				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) {
				//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.
	//console.log("async globs");
	await zaplists.update();
	page_erasedElems=(await getValue(webpage+":erasedElems","")).trim();
	site_erasedElems=(await getValue(website+":erasedElems","")).trim();
	elems_to_be_hid=getHidElemsCmd();
	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={};
}

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;
	}
	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.stopImmediatePropagation();    }}					       

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

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 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());
	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);
	}
	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) { //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); 
	}
}

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 extend_jquery() {
	$.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();
				}
			}
		}; })($); //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){console.log("Can't get selector for",desc,e.lineNumber,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.
	
}

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) { // 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.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,8,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) {
	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%;'>");
	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%;' src="+rsrc+"></img>"), 
		curtainRod=$("<sfswediv tabindex=0 class=WebEraserCurtains cc="+(++curtain_cnt)+" style='z-index:2147483640; position:absolute; display:block; opacity:0.94;'></sfswediv>"),//!!overflow:hidden; rm'ed //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.contextmenu(e=>(eraseElementsCmd(),false));
	lrcurtains.click(function({ctrlKey:ctrl,shiftKey:shift,altKey:alt,target:target}) {
		if (!(alt||shift)) return;
		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);
		else if (ctrl&&alt) 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;   //console.log("zap inited:",key,keytype);
	
	//console.log("zaplist created key:",key,", keytype",keytype,", list",list);
	
	this.add=async function(str,kl) {
		if (!!kl != !!keytype) return;
		if((await 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=async function() {
		list=await readlist();
		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]) {
	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]; 
	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,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) {
		requestAnimationFrame(tstamp=>objs.css(css_attr,plotvals[cnt]+suffix));
		if (++cnt==noOf_subintervals) {     
			clearInterval(i);  CB && CB.call(that);}}
}

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, else from gm4-polyfill and returns a string "contextmenu";
			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 Erase Web Elements 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).",function(){
		setValue(website+":erasedElems","");
		setValue(webpage+":erasedElems","");
		location=location;
	}];
	if(!GM_registerMenuCommand(...reg_args))
		GM.registerMenuCommand(...reg_args);
	regcmds.done=2;
} // regcmds()

function setValue(n,v) { 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
	//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 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= !GM.getValue; } // || "Barychelidae"!=GM_getValue("arachnoidal","Barychelidae"); }
	catch(e) { environ.nonGMmode=true; }; //eg, chromium stadalone
	environ.nonjQ = !window.jQuery || parseFloat(jQuery.fn.jquery) < 3.1;
	
	if (nonjQ||nonGMmode){ //chromium bare, ie, w/o tamper.
		console.info("WebEraser userscript in non GM_ mode at "+location.href, "typeof GM:",typeof GM, "nonjQ:",nonjQ,"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 "+e,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 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();
		};
		if(jq_versions_prior.core < 3.1)
			xhr_queue.push("https://code.jquery.com/jquery-3.1.1.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.js");
		xhr_queue.push("https://raw.githubusercontent.com/SloaneFox/code/master/GM4_registerMenuCommand_Submenu_JS_Module.js");
		xhr.open('GET', xhr_queue.shift()); xhr.send();
		return true; 
	} else  // if (nonGM || nonJQ)
		return false;
} //environInit()