slow! / WebEraser

// ==UserScript==
// @name        WebEraser
// @version     1.6.4
// @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-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://raw.githubusercontent.com/SloaneFox/code/master/sfs-utils-0.1.6.js
// @run-at      document-start
// @icon        https://raw.githubusercontent.com/SloaneFox/imgstore/master/WebEraserIcon.gif
// @author      Sloane Fox
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_xmlhttpRequest
// @grant       GM_getResourceURL
// @grant       GM_addStyle
// @grant       GM.registerMenuCommand
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.deleteValue
// @grant       GM.listValues
// @grant       GM.addStyle
// @grant       GM.xmlHttpRequest
// @grant       GM.getResourceUrl
// ==/UserScript==

//
// History
// updated Nov  2020  v1.6.4   CORS blocking sites require the loading of js files via the header, not dynamically.
// updated Oct  2020  v1.6.0   Performance profiling and moved includes (userscript requires & resources), reducing page load time.
// updated Sept 2020  v1.5.6   Moved to TamperMonkey instead of GreaseMonkey, hence additional function in main script menu to Export/Import script's data.
// updated Sept 2020  v1.5.1   Added new menu options to erase Iframes, those that are not clickable, it gets the user to hover mouse over item to detect iframe.
// updated Mar  2019  v1.4.10   Change in 'temp-mode' to allow quick deletion of Cookie banners, modals, etc.  Minor aesthetics.  Wrap structure changed.  Version 9->10 minor Chromium bug report fixed.
// 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).

var requires_hdr_str=`
// @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/sfs-utils-0.1.6.js
`
requires_hdr_str=""; //!!  // Downloading problematic when CORS server restrictions apply.

var resources_hdr_str=`
// @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
`;

ttimer("w/e start");

// Globals:
var jq_saved=window.jQuery; //window; //this // note 11'17 polyfill acts upon this (sandbox with a member "window") not on window.
 //Closure used with jq_saved since jQuery in window object can be corrupted when other jq modules not loaded.

var iframe=window!=window.parent, border_width=6;
var win=window,
	host=window.document.location.host,
	pathname=window.document.location.pathname, dynamic_load_complete,
	webpage=host+pathname, website=host;  // used as keys for saving/reading user set erasure data.
var tempMode,last_ones_deleted=[], delcnt=0, trace_elem, sel_matching_els, gpre_elem,
	bblinker, promptOpen, rbcl="sfswe-redborder", tbcl="sfswe-transparentborder";
var tab="&emsp;&emsp;&emsp; &emsp; "; // tab=5spaces, emsp=4spaces, but HTML tab in a <pre> wider hence extra emsp's.

var script_name, log;  // Script-local vars set in loaded js file.  Declaration here is to ensure it remain local to this userscript.

//
// 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,whitecurtainsxsm;
var ignoreIdsDupped, curtain_cnt=0;
var zaplists,overlay=false;

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

// var turl="https://code.jquery.com/jquery-3.2.1.js";
// var p=pledge();
// GM_xmlhttpRequest({	
// 	method: "GET",	url: turl, onload: function(response) {
// 		p.resolver(response.responseText);
// 		console.log("MLINE xmlHttpRequest res:",response.responseText.substring(0,40));}
// });

// (async x=> { eval(await p); console.log("TPEOF ", typeof $); })();

// console.log("did get for ",turl);

loader();
async function loader() {
	await environInit();
	await preMain();
	if(/^complete/.test(document.readyState)) main();
	else document.addEventListener("readystatechange",main);        //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; };  // prefix given arg if string there.

async function init_globs_phase_1() { // all globs asynchronously set.
	page_erasedElems=(await getValue(webpage+":erasedElems","")).trim();

	site_erasedElems=(await getValue(website+":erasedElems","")).trim();
	config=await getValue("config",{keepLayout:"checked",monitor:{}});
	if (!config.monitor) config.monitor={};

	elems_to_be_hid=getHidElemsCmd();
}

async function preMain() { //attempt to get in at document-start, either preload of js or of images slows page load too much.  However, eval of js files will not help.
	await init_globs_phase_1();
	ttimer("preMain()-inited globs");
	installEventHandlers();
	regcmds();
	ttimer("preMain-half");
	if(elems_to_be_hid=="" && !nonGMmode) {console.log("WebEraser, nothing erased.");return; }
	await mainette();                    ///////////// mainette loads @require files.
}

async function main() { try{
	ttimer("main()-start");
	//console.log("w/e main GM:",typeof GM, "readyState",document.readyState,"body:",document.body,"iframe",iframe,"jQuery:",window.jQuery&&window.jQuery.fn.jquery,"$",window.$);
	inner_eraseElements("init");
	var nerased=$(".Web-Eraser-ed").length, delay=5000+300*(2+nerased), forErasure=getHidElemsCmd("count");
	setTimeout(x=> { 
		ttimer("start of phase 2 "+delay+"ms later ");
		//console.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"); // for iframes that delay in loading.
		ttimer("end delayed phase2");
	},delay);
	ttimer("main()-end");
} catch(e){console.error("WebEraser main(), error:",e);}} //main()
 
async function mainette() { try{ // called from main and form click handler, completes main when have els to erase.
	if(mainette.done) return; mainette.done=true;
	ttimer("mainette()-start");
	await dynamicLoadRequires();  // sets log()
	await init_globs_phase_2(); // depends on load of js polyfill file on FF for 'GM.' funcs.
	//dynamic_load_complete=true; //!!
	//cmdrepl();
	//GMDataEditor(); // adds menu option perhaps prior to completion of submenuModule.
	log=x=>null;
	regcmds(); 
	ensure_jquery_extended(); // may get clobbered by other script loading jQ.
	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; }'); //, "we-jquery-dialog-css"); 
	// A later defined rule has precedence when both rules in effect.
	
	//setTimeout(inner_eraseElements,1500);
	setTimeout(reattachTornCurtains,3000);
	sel_matching_els=$();
	ttimer("mainette()-end");
} catch(e){console.error("WebEraser mainette(), error:",e);}} //mainette()

async function handleClick(e, iframe_click) { 
	if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
	prevDef(e);
    if(e.type!="mousedown") return;
	await mainette();
	//console.log("ctrl-CLICKed on element:",e.target,"Event:",e);
	var target=e.target, related=e.explicitOriginalTarget||e.relatedTarget;
	if(/body/i.test(e.target.tagName) && related) target=related;
	userIOtoHideElement(target,e,iframe_click);
}

function userIOtoHideElement(target,e,iframe_click) { try { //called from event handler in page & iframe, and pseudo called from click within iframe.
	halter(); // to handle an unload
	var permrm, frameEl=target.ownerDocument.defaultView.frameElement;
	if(frameEl) target=frameEl;
	//prevDef(e);//see above
	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") && !tempMode) 
		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 (!tempMode) {
		if ($("body").is(target)) {
			console.log("Click was on element,",target,"Event:",e);
			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 'OK' 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.
			//console.log("then in checkIfPermanentRemoval, permrm:",permrm,"confirm_val[bool,sel]:",confirm_val);
			var [permrm,item_sel]=confirm_val;
			halter.dialog=false;
			unhalt();
			if (permrm==Infinity)      { // sprompt returns Infinity for when extra button (3rd) is clicked.  temp-mode delete
				tempMode=true; 
				alert("\tYou have entered Temporary-Deletion mode.\n\n\tCtrl-click will now merely remove elements temporarily.\n\nEsc to undo deletions.  Reload to revert.");

				let target=$(item_sel);
				last_ones_deleted.push(target);
				let pholder=$("<placeholder delcnt="+(++delcnt)+">");
				target.replaceWith(pholder);
				escapeCatch(function(){ 
					if(delcnt==0) return;
					let last_one=last_ones_deleted.pop();
					$("placeholder[delcnt="+delcnt--+"]").replaceWith(last_one); 
					last_one[0].scrollIntoView();
				},"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 !tempMode
	else if( ! $(target).is("body")) { //temp-mode
		let t=$(target);
		last_ones_deleted.push(t);  
		t.replaceWith("<placeholder delcnt="+(++delcnt)+">");
		console.info("Temp-mode deleting:",t,t.text());
	}
	if(!halter.dialog) { unhalt();}
	return false;
} catch(e) { console.log("W/e Click handling error:",e,"\nLineNumber:"+(e.lineNumber),"Stack:",e.stack);unhalt(); }}  //handleClick() 

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);}
function prehitch(target) {         // hitch up hierarchy if click is on element that cannot be red-bordered.
	var svg=target.closest("svg");          // DOM method dom-Elem.closest("sel1, sel2, ...").  Also method matches("sels");
	if(svg) target=svg; 
	return target; 
}
function checkIfPermanentRemoval(target) {   // called from click handler & eraseIframe.
	var sconfirm_promise, checkif_resolve, checkif_reject;
		checkif_promise=new Promise((resolve,reject)=>{      // checkif_promise is returned by this function, checkIfPermanentRemoval().
			checkif_resolve=resolve; checkif_reject=reject;
			console.log("checkIfPermanentRemoval target:",target,"size",$(target).height(),$(target).width());
			target=prehitch(target);
			var parent=target.parentNode, index=0;
			var msg="Permanently erase selected element(s) from website&mdash;now seen on page red-bordered and blinking?  "
				      //+"\n\nIn addition you may use 'w' and 'n' keys freely, to widen and narrow your selection.  "
			          //+"\n\nOpen the script's main menu via this link:&nbsp;<a href='#abc"+Math.random().toString(36)+"'>WebEraser Prefs...</a>, to configure in detail.  " // Clickable link see .click below.
				+"<span title='Clink heree to open main preferences window.' class=sfs-link>Preferences...</span>" // Clickable link see call to eraseElementsCmd in .click() below.
				  // +"Hit 'Temp Mode' button below for ctrl-click to erase element(s) temporarily and inhibit this prompting until reload."
				+"\n\n"
				+"<div style='display:none;; position:relative;width:100%'>"   //inline-block
				+"  <span id=fsfpe-tagel>Internal code for</span><br>"
				+"  <input id=sfswe-seledip disabled style='color:gray;width:80%;margin:10px;'>"
				+"  <div id=sfswe-seledipfull style='position:absolute; left:0; right:0; top:0; bottom:0;'></div>"
				+"</div>";

			$(document).keypress(keypressHandler);
			var tooltip="To refine the choice of what to erase, one can widen or narrow the choice when elements are nested.\nTo invoke this function hit the 'w' and 'n' keys respectively to stagewise widen and narrow your choice."
				+"The red borders will expand/narrow to show the current selection, if the red borders blink fast, this means it is at its limit.\nSee console for WebEraser info giving chosen element's selector.";

			sconfirm_promise=sconfirm(msg,"Cancel","OK",null,tooltip); //////////////// ,"Temp Mode");
			var dialog=sconfirm_promise.dialog; // dialog is classed .ui-dialog.
			//dialog.find(".ui-button:last").attr("title","Temp mode, from now on ctrl-click quickly deletes elements; in effect until page reload.");
			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;float:left;'><input id=sfswe-checkbox6 type=checkbox style='vertical-align:middle'>"	
							  +"<label style='display:inline;'>&nbsp;&nbsp;Completely delete element.</label></div>");
			buttonpane.append("<br><div style='margin-top:-10px;'><input id=sfswe-checkbox10 type=checkbox style='margin-left:30px;vertical-align:middle'>"	
							  +"<label style='display:inline;' "
							  +"title='Temporary-Deletion mode means that when ctrl-click is pressed it will now quickly delete any element on the page.  "
							  +"However, the deletion is temporary and when the page is reloaded all is restored and ctrl-click will then behave as before.'>&nbsp;&nbsp;Enter Temporary-Deletion mode.</label></div>");

			dialog.find(".sfs-link").click(e=>{
				log("click on a in OK");
				dialog.trigger($.Event("keydown",{keyCode:27,key:"Escape"})); // close prompt and open Prefs dialog.
				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){                 ///////////////////////////////////////
		//console.log("Prompt closing, reply:",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($("#sfswe-checkbox10:checked").length!=0 && reply) {
			hlightAndsetsel(0,"off","restore"); 
			checkif_resolve([Infinity, $("#sfswe-seledip").val().trim()]);  // Infinity means 3rd button which was for tempMode.
			return;
		}
		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        /////////////////////////////////////////////
}

async function eraseIframe() {
	await mainette();
	if(!await sconfirm("\nTo erase a page element, click OK below, then place and leave the mouse hovering over the chosen element for 6 seconds."
                       +"\n\nThis function is used when ctrl-click does not work.  "
					   +"Certain items visible on the page, eg, Iframes cannot be clicked.  "
					   +"Clicking on them results in a new page opening.  ")) return;
	var current_hover_targ;
	window.addEventListener("mouseover",moverScan,false);

	function moverScan(e){ 
		current_hover_targ=e.target;
//		if(/iframe/i.test(e.target.tagName)) {
		setTimeout(function(prev_targ){
			if(prev_targ==current_hover_targ) {
				window.removeEventListener("mouseover",moverScan,false);
				userIOtoHideElement(e.target,e);
			}
		},6000,current_hover_targ);
		//}
	} // moverScan()
} // eraseIframe()

async function eraseElementsCmd() { 
	// Called from GM script command menu (Preferences...) and from clickable within ctrl-click  prompt.
	// 
	await mainette();
	var erasedElems, no_sels;
	erasedElems=getHidElemsCmd("with site");
	no_sels = !erasedElems ? 0 : erasedElems.split(/,/).length;

	//	"See checkboxes distantly below to set the script's configutation values."

	var prompt_promise=sprompt(
		"<ul style='padding:0;'>Usage:</ul>Whilst holding down the Control key, make a mouse click on the webpage&mdash;after quitting this dialogue.  That will open a dialog to confirm the erasure.  "
		+"\n\nOn some elements however, ctrl-click is absorbed.  In that case choose from this script's "
			+"menu:&nbsp; <span class=sfs-link>Erase via mouse hover....</span>"
			+"\n\nBelow find an editable, comma-separated list of each deleted element.  Below that see the preferences' checkboxes.\n"
			+"\n"+( no_sels ? "Currently there's "+no_sels+" below.  " : "No element is being deleted.")+"\n\n"  //"To remove all element erasures set to blank.\n\n"
		, erasedElems.replace(/,/g,", \n"));
	
	prompt_promise.then(async function([btn,reply_text]){ 
		if (!btn) return; //btn is null when Cancel is hit, true for OK, undefined when 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-checkbox8:checked").length)   config.ctrl_e_ON="checked";
		if ($("#sfswe-checkbox9:checked").length) {
			let nvs=[], array_of_keys=await GM_listValues();
			for (let n of array_of_keys) {
				nvs.push( { name:n, value_str: await GM_getValue(n) } );
			}
			sprompt("Stored data, export by copying the entire string below.  Import by pasting that entire string below.", JSON.stringify(nvs,null,"\t"))
				.then(async function([btn,reply_text]){  // pretty print json
					if(!btn) return;
					let new_nvs=JSON.parse(reply_text);
					for(let obj of new_nvs) {
						alert("setting with given name:"+obj.name+", value:"+obj.value_str);
						await GM_setValue(obj.name, obj.value_str);
					}
				});
		}
		else 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 {  //box4 not checked.
			let duplicates={};
			reply_text=reply_text.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_text);
			if(reply_text.length) {
				page_erasedElems=[]; site_erasedElems=[]; 
				$(reply_text).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_text);} 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");
		}// end else
	});//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;vertical-align:middle;"+(!keep_layout?" margin:0 3px;":"")+"' "+keep_layout+">&nbsp;<label>Preserve layout (in general).</label>"
			+(keep_layout ? "<input id=sfswe-checkbox3 type=checkbox style='vertical-align:middle;margin:0 3px 0 10px;height:12px;'"+(config.hideCurtains||"")+">&nbsp;<label title='Leaves a blank space over deleted item.'>Also hide curtains.</label>" : "")
			+"<br><input id=sfswe-checkbox type=checkbox style='vertical-align:middle; margin-left:3px;'"+(config.noAnimation||"")+">&nbsp;<label>Disable animation (in general).</label>"
			+"<br><input id=sfswe-checkbox4 type=checkbox style='vertical-align:middle; margin-left:3px;'>&nbsp;<label title='Check box then hit ok'>Set your own curtains' image.</label>"
			+"<input id=sfswe-checkbox5 type=checkbox style='vertical-align:middle;margin-left:15px;'"+(config.monitor[website]||"")+">&nbsp;"
			+"<label>Monitor this website for new elements.</label>"
			+"<br><input id=sfswe-checkbox8 type=checkbox style='vertical-align:middle;margin-left:3px;'"+(config.ctrl_e_ON||"")+">&nbsp;<label>Set ctrl-e to invoke erasure.</label>"  //ctrl_e_ON
			+"<input id=sfswe-checkbox9  type=checkbox style='vertical-align:middle; margin-left:15px;'>"+"&nbsp;<label title='Check box then hit ok'>Export/Import script's stored data.</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");
	dialog.find(".sfs-link").click(e=>{
		log("click on span");
		dialog.trigger($.Event("keydown",{keyCode:27,key:"Escape"})); // close prompt and open Prefs dialog.
		eraseIframe();});
	
} //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");
	//console.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.
		if (erasedElems.length==0) { erasedElems=$(stripClasses(sel)); if(erasedElems.length!=1); erasedElems=$(); }
		erasedElems.each(function() {
			//console.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));
		//console.log("Q:",document.querySelectorAll("body *"),typeof $$)
	}
	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 > 90% (was 0.6) of window, deleted."
			  : onzaplist.zap ? " => complete erasure."
			  : onzaplist.keep_layout ? " => erase but keep layout."
			  : "" );
	});
	ieemsg+="(phase:"+from+")";
	count=0;
	console.info(ieemsg);
}

function closeCurtains(el, noAnimKeepLayout, finishedCB=x=>x) {   //called from inner_eraseElements(), creates curtains if need be.
	//console.log("closeCurtains1, el:",el,noAnimKeepLayout,"CB:",finishedCB,"sel:", el.attr("selmatch-sfswe"), el[0].tagName);           //,"\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.siblings("sfswediv"), curtainRod, lrcurtains;
	var old_curtained=wediv.length ? jQuery.data(wediv[0],"covered-el") : null;
	if ( ! old_curtained || ! old_curtained.is(el))
		[curtainRod,lrcurtains]=makeCurtains(el,noAnimKeepLayout);
	else { curtainRod=el.siblings("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++;
		ttimer("start-animation");
		manimate(lrcurtains,["width",15,"%"],1000,2);
		manimate(lrcurtains,["width",51,"%",1000],1000,5,function(){ ///////////////////////Animation
			ttimer("end-animation");
			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) finishedCB(curtainRod);
				
			
		}); //animate()
	}
	return false;
} //closeCurtains()

function keypressHandler(event) { //try {  //while prompt is open.
	switch(event.key) {
	case "w": widen(); break;
	case "n": narrow(); break; 
	default: return; } 
	return false;
}

async function init_globs_phase_2() { // all globs asynchronously set.
	ttimer("init phase2-start");
	zaplists=new zaplist_composite(); 
	await zaplists.update(); //depends on site/page_erasedElems being read first.

	ownImageAddr=await getValue("ownImageAddr","");
	// The following instruction when in TM using GM_ version, is very slow, it returns a base64 string, 2,928,417 bytes in length.  Slow on chromium/TM, ok on FF.
	whitecurtains=await getResourceUrl("whiteCurtains");

	// Ensure visit to https matches getResourceUrl use of https or address as given in header w/wo ssl!
	whitecurtainsoriginal=await getResourceUrl("whiteCurtainsOrig");
	whitecurtainstriple=await getResourceUrl("whiteCurtainsTrpl");
	whitecurtainsxsm=await getResourceUrl("whiteCurtainsXsm");

	curtain_icon=ownImageAddr|| whitecurtains;
	curtain_slim_icon=await getValue("ownImageAddr","")||whitecurtainsoriginal;
	curtain_xslim_icon=await getValue("ownImageAddr","")||whitecurtainsxsm;

	curtain_wide_icon=await getValue("ownImageAddr","")||whitecurtainstriple;
	ttimer("init phase2-end");
}

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 { // phase2
		$("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", postMessageHandler,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;
	console.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)=>{ 
	for (let el of document.querySelectorAll("iframe, embed")) {
		if (el.contentWindow==e.source ||e.data.src==el.src) { iframeEl=el; return false; } // return as early terminate.
	};

	handleClick({target:iframeEl,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"), nnnid=nearestNonNumericId; //closest also checks target
	//console.log("nnnid",nnnid, "matches #els:",$("[id="+nnnid+"]").length);
	if (nnnid && $("[id="+nnnid+"]").length>1) { nnnid="";ignoreIdsDupped=true;} // Page error duplicate ids, ignore id.
	if (nnnid) nnnid=$("#"+nnnid).prop("tagName")+"#"+nnnid; //cos of jQ & multiple ids.
	if ($(nnnid).is(target)) sel=nnnid;
	else {
		sel=selector(target,$(nnnid),true,0,exclude_classes); //ok if nnnid is undefined id.
		if (!sel) sel=nnnid; //both target and $(nnnid) are same element. 
		else if(nnnid && !/^html/i.test(nnnid)) sel=nnnid+sel; 
	}
	//console.log("getSelectorWithNearestId got sel", sel, "matching:",$(sel)[0],"for el:",target);
	return sel;
}

function getHidElemsCmd(cmd,el){
	var els, pels=page_erasedElems, sels=site_erasedElems;
	switch(cmd) {
		
	case "match el":  return el.is($(getHidElemsCmd()));
	case "isPage":    return el.is($(pels));
	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);
	}
	console.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=trace_elem;
		trace_elem=$(elem);
		var newsel,fullsel,h=trace_elem.height(),w=trace_elem.width();
		if (!mere_highlight) { //not typed in but from widen/narrow etc.
			var selinput=$("#sfswe-seledip"),            //sfs_pesel");
				elhtml=trace_elem[0].outerHTML.replace(trace_elem[0].innerHTML,"");
			let exclude_classes=tbcl+" "+rbcl+" Web-Eraser-ed";
			newsel=getSelectorWithNearestId(trace_elem, exclude_classes);
			fullsel=               selector(trace_elem,0,false,0,exclude_classes);
			sel_matching_els=$(newsel); //.not(trace_elem);
			//if($(sel_matching_els).length==0) sel_matching_els.match(/\.[^.>\s]+/g)
			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" : "")
						  //+trace_elem[0].outerHTML.replace(/>.*/g,">").replace(/\s*</g,"<")
				+"Element html:\n"+elhtml
				+"\n\nElement style:\n"+myGetComputedStyle(trace_elem[0]));
			console.info("WebEraser info: "+sel_matching_els.length+" element(s) highlighted has selector:\n\t\t",newsel);
			console.groupCollapsed(sel_matching_els.length);console.log(sel_matching_els.toArray()); console.groupEnd();
			//,"\nMatching els:",sel_matching_els);
			//console.log("fullsel",fullsel,"matches:",$(fullsel).length);

		} //endif !mere_highlight
		updatePromptText(newsel,fullsel,"hide");
		trace_elem.data("pewiden-trace","true"); //    if (!trace_elem.hasClass("pewiden-trace"))
		sel_matching_els.addClass(rbcl);
		trace_elem.elh=trace_elem[0].style.height;	trace_elem.elw=trace_elem[0].style.width;
		trace_elem.height(h- 2*border_width);trace_elem.width(w- 2*border_width);
		bblinker=setInterval(function(){ // normal "selected" blink.
			if (sel_matching_els.length) sel_matching_els.toggleClass(rbcl);
			else trace_elem.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);
		sel_matching_els.removeClass(rbcl);
		trace_elem[0].style.height=trace_elem.elh;	trace_elem[0].style.width=trace_elem.elw;
		//trace_elem.height(h+ 2*border_width);trace_elem.width(w+ 2*border_width);
		if (restore) $("."+tbcl).removeClass(tbcl); 
	}
}catch(e) {console.error("hlightAndsetsel(), error:",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");
	selinput.parent().show();

	var sel=selinput.val(); 
	if (/[:.][^>]+$/.test(sel)) { // special case of widen, not just jump to parent. Regexp matches end of sel as eg, ":nth-of-type(2)"
		var newsel=selinput.val().trim().replace(/[:.][^:.]+$/,"");  // Remove eg, ":nth-of-type(n)", matches now multiple elements.
		selinput.val(newsel);
		sel_matching_els=$(newsel);
		sel_matching_els.addClass(rbcl);
		log(":nth-child-of Widen, new sel:",newsel,"old sel",sel,"Trace_elem",trace_elem[0]);
		updatePromptText();   
		return;                // No call to hlightAndsetsel, hence no trace of nth-of-type etc. left, a subsequent narrow will jump back.
	}
	var p=trace_elem.parent();
	if (p.is("body")) {
		blinkBorders($("body")); //blink double indicates top of hierarchy.
		return;
	}
	hlightAndsetsel(0,"off");
	hlightAndsetsel(p);
}

function narrow() {
	var selinput=$("#sfswe-seledip");
	selinput.parent().show();  // show extra detail in user prompt, selector update.
	var trace=trace_elem.find(":data(pewiden-trace):first"); // trace left by hlightAndsetsel()
	if(trace.length==0) trace=trace_elem.find(">:only-child");
	if (trace.length==0) {
		blinkBorders(trace_elem);
		return;
	}
	hlightAndsetsel(0,"off");
	hlightAndsetsel(trace);
}


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

function updatePromptText(newsel,fullsel) { 	// set text size tagname etc.
	var updated_text="";
	if (sel_matching_els.length<=1)
		updated_text="selected ("+trace_elem.prop("tagName").toLowerCase()+") element ("+(trace_elem.height()|0)+"x"+(trace_elem.width()|0)+"pixels)";
	else
		updated_text="selected "+sel_matching_els.length+" "+trace_elem.prop("tagName").toLowerCase()+"s";
	updated_text+=":";
	var extra_msg=$("#fsfpe-tagel").parent();

	extra_msg.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 ensure_jquery_extended() { 
	if ($.expr[":"].regexp && $.fn.reverse) 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()     //use eg: $(“div:regexp(className,promo$)”);

	(function($){ 
		$.event.special.destroyed = {
			remove: function(o) {
				if (o.handler) {
					o.handler();
				}
			}
		}; })($); //Use: $("#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(">");          

	//console.log("selector(), got sel:",sel, "found:",$(sel)[0],"for el:",desc);

	if (desc.is(anc.find(">"+sel))) {
		if (anc.is(document.body.parentNode)) sel="html>" + sel;
		else sel=">"+sel;
	} else {
		console.info("Selector result:\n\t"+sel+"Not findable in ancestor, nor in body's parent.");
		if ($(sel).length==0) sel=undefined;             // Its the very top element, <HTML>.
	}
	//sel=removeUselessClasses(sel);
	//if ($(sel).length==0) sel=stripClasses(sel);
	return sel;
} catch(e){logError("Can't get selector for ",e,"Element:",desc[0]); }}  //fixBadCharsInClass(desc);}

function stripClasses(s) {
	var stripped=s.replace(/\.[^.>\s]+/g,"");
	console.log("Forced to strip classes for:",s,"Stripped:",stripped,"matched:",$(stripped).length);
	return stripped;
}


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("torn");
} catch(e){console.error("WebEraser reattachTornCurtains(), error@",e.lineNumber,e);}}

function measureForCurtains(curtains=$(".CurtainRod")) {
	curtains.each(function(){
		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) throw "Demi-err, no-element in measureForCurtains";  //{console.error("Demi-err, no-element in measureForCurtains");el=$();return;}

		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 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") {
				target.siblings("sfswediv").css("display","none");
				measureForCurtains();
			} else if (currval.display!="none" && (oldval.display=="none" || oldval.display==undefined )) {
				target.siblings("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) {
				measureForCurtains();
			} else if ( parseInt(currval.width)|0 - parseInt(oldval.width)|0) {
				measureForCurtains();
			} else if (currval.visibility!=oldval.visibility)
			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);
				markForTheCurtains(jQnode,node,foundsel);
				closeCurtains(jQnode,false,measureForCurtains); //nomonitor(0); },300);
			} else { // node removed
				$(".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"
	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");
			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 design:
//
//    <sfswediv tabindex="0" class="CurtainRod outie" cc=1 jQ.data("covered-el")>
//       <img class="WebEraserCurtain sfswe-left">
//       <img class="WebEraserCurtain sfswe-right">
//    </sfswediv>
//    <XYZ id=ApageEl class=Web-Eraser-ed selmatch-sfswe="XYZ#xyz" jQ.data(curtainRod)> // target el, for covering.  

//
/////////////////////////////
// old style:
// <DIV id=xyz class=Web-Eraser-ed selmatch-sfswe="DIV#xyz"> // target el, for covering.  el.siblings("sfswediv") has data covered-el to here.
//   <sfswediv class=CurtainRod cc=n data.covered-el=divtarget>             
// 	    <img class=webEraserCurtain sfswe-left>                        
// 	    <img> class=webEraserCurtain sfswe-right>
//   </sfswediv> 
// </DIV> 
//
// 
// Outline of stored data in script handler:
// key                        value
// ‾‾‾                        ‾‾‾‾‾ 
// <website>:erasedElems      The list of website and paths to erased elements [array of strings]
// <webpage>:erasedElems      Ditto for webpages [array of strings]
// config                     The script's settings/configurgation values [object]
// ownImageAddr               url of custom cutain image [string]
// <website>:zaplist          These 4 values each hold a list of those elements that are to be completely removed and/or those that keep layout (kl)
// <webpage>:zaplist
// <website>:zaplist:kl
// <webpage>:zaplist:kl


function makeCurtains(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("makeCurtains ",noAnimKeepLayout,"h/w",h,w," el:",el,"Count:",curtain_cnt);

	// 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 randkey=Math.random().toString(36).substring(7);
	var rcurtain=$("<img class='WebEraserCurtain sfswe-right' style='right:0;position:absolute;height:100%;visibility:visible;' src="+rsrc+"></img>"), 
		curtainRod=$("<sfswediv tabindex=0 rkey="+randkey+" class=CurtainRod cc="+(++curtain_cnt)+" style='z-index:2147483640; position:absolute; display:block; opacity:0.97;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+
		(getHidElemsCmd("isPage",el) ? ", webpage is: "+webpage : ", website is: "+website)+".";
	
	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({covered-el: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],"covered-el",el);
	jQuery.data(el[0],"rod-el",curtainRod);
	//console.log("makeCurtains, data covered-el", jQuery.data(curtainRod[0],"covered-el"),"for rod",curtainRod[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=Math.min(w,window.innerWidth)*Math.min(h,window.innerHeight);
		if (visible_area>=warea*0.9) { // 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,input,textarea")) { 
	// 	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 { 
	// jg rm 8l, trial, all outies
	curtainRod.addClass("outie"); // ROD is <sfswediv> 
	let pos=moffset(el);
	curtainRod.css({height:h,width:w}).css(pos); //	curtainRod.css({height:"100%",width:"100%",left:0,top:0});
	//	el.wrap("<div class=sfswe-contner>");
	let r=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);
	} 
	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) 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,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{
		//console.log("eppursimuove",cnt,objs,plotvals[cnt],suffix);
		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,reg_args2;
	reg_args=["WebEraser Preferences....     ["+(elems_to_be_hid?"some erased":"none erased")+"]", eraseElementsCmd,"","", "E"];
	reg_args2=["WebEraser Erase via mouse hover....", eraseIframe,"","", "C"];

	if(dynamic_load_complete && nonGMmode) {
		await submenuModule.register("WebEraser");
		registerMenuCommand(...reg_args); 
		registerMenuCommand(...reg_args2);
		submenuModule.showMenuIcon();
	}

	if(regcmds.done) return;
	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.
			console.log("Using dot form");
	}	

	if(!GM_registerMenuCommand(...reg_args2))  // from GM4_registerMenuCommand_Submenu_JS_Module, if there, undefined, else from gm4-polyfill which returns the menuitem DOM object.
		GM.registerMenuCommand(...reg_args2);  // from gm4-polyfill.  It sets body contextmenu style menu.
	regcmds.done=true;


	//tmp bridge Sept'20
	let ctrl_e_ON=await getValue("eraseElems_ctrlE",false);
	if(ctrl_e_ON) { config.ctrl_e_ON=true; deleteValue("eraseElems_ctrlE"); }
	
	if(config.ctrl_e_ON) document.addEventListener("keydown",function(e) {
		if (!e.ctrlKey || e.key!="e") return;
		setTimeout(function(){salert("Invoking web erasure function.");},300);
		inner_eraseElements("fromCtrlE");
	});
	regcmds.done=true;

}// regcmds() 


function sprompt(tex,initv,cancel_btn="Cancel",ok_btn="OK",extra_btn,tooltip){ // 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,tooltip);
	});
	p.dialog=dialog;
	return p;
}
function sconfirm(msg,cancelbtnText,okbtnText,extrabtnText,tooltip) { 
	return sprompt(msg,undefined,cancelbtnText,okbtnText,extrabtnText,tooltip); 
}
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,tooltip) {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 class=sfswe-pretext>"+pretext+"</div>" 
				  //+pretext
				  +(initval!==undefined ? "<"+input_tag+" class=sfs-input 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({
		draggable:false, modal: true, width:"auto", resizable: false, 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();
	if(tooltip) { content.attr("title",tooltip); } // must set after call content.dialog().

	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 ( $("body").css("pointer-events") == "none") $("body").css("pointer-events","auto");
	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"
	var titlebar=dialog.find(".ui-dialog-titlebar");
	titlebar.find("button").remove(); titlebar.height(8);
	titlebar.add(".sfswe-pretext").css("cursor","move");
	dialog.draggable({ cancel: ".sfs-input" }); // needs to unset draggable in dialog setup first.
	setTimeout(x=>content.focus(),100);
	that.last_dfunc=dfunc;
	return dialog; //.ui-dialog
}catch(e) {log("hlightAndsetsel(), err",e.lineNumber,e);}}

// Layout/outline of sprompt:
// 
//  <div class=sfswe-prompt>                                    // jQ dialog uses namespace + "-prompt" for class name.
//       <div class='ui-dialog' role=dialog>
//            <div class='sfswe-content ui-dialog-content'>
//                 <div class=sfs-pretext>
//                 <input> or <textarea> of class sfs-input.
//            <div class=ui-dialog-buttonpane>
//

function setValue(n,v) { 
	if (!v) 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("getvalue(): Error with key:"+n+" parse of value:"+res+".Value:"+v+".  Error:",e); return v; } }

function deleteValue(n) {
	return setValue(n,null);
}

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

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 logStack(depth){var e=new Error; return e.stack.split(/\n/).slice(2).slice(0,depth);}

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 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.  If PERM is set eventlistener is not remove after first Esc.
	var that=escapeCatch;
	if(!that.flist || that.flist.length==0) {
		that.flist=[cbfunc];
		window.addEventListener("keydown", subfunc, true);
	} 
	else 
		if(cbfunc) { that.flist.push(cbfunc); return; }

	function subfunc(e) { 
		if (e.which == 27)  {
			console.log("escape",perm," is:",that.flist);
			that.flist.forEach(func=>func());
			if(perm) return;
				window.removeEventListener("keydown", subfunc, true); 
			that.flist=[];
		}
	} // subfunc();
}


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() {
	var sfslink_css=`.sfs-link {      cursor:pointer;  color:navy;    }
                     .sfs-link:hover {       text-decoration:underline;   }`;

	return sfslink_css+".ui-dialog-content,.ui-dialog,.ui-dialog textarea { font-size: 16px; font-family: Arial,Helvetica,sans-serif; border: 1px solid #757575; " //+"background:whitesmoke;
		+" color:#335; padding:12px;margin:5px;} "
		+".ui-dialog-buttonpane {  background-color: inherit;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,.ui-dialog-buttonset, .sfswe-ticks * {  padding:unset;width:auto; font-weight:unset; display:inline; }"  // margin:auto;    Even w/o resizeable being set for dialog, this comes in frmo jq-ui, and at example.org divs are set wildly, the handle is a div.
		+ (str=>str+str.replace(/-moz-/g,"-webkit-"))(
			".sfswe-content :-moz-any(div,input) { font-size:13px;padding:0px;margin: 0;color:#333; opacity:1;  }" //background:whitesmoke; 
				+".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
}

async function environInit() { // returns false if GM environment is there, otherwise it calls main when ready and immediately returns true.
	this.plat_chrome=false; this.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)) this.plat_chrome=true;
	this.plat_mac = /^Mac/.test(navigator.platform);
	this.nonGMmode=false;
	try { 
		if(typeof GM_getValue=="undefined" || typeof GM_deleteValue=="undefined") {
			check_GM_Support(GM.getValue);
			this.GM_getValue=GM.getValue; // setup getValue for others load polyfill when needed.
			this.GM_setValue=GM.setValue; // ditto.
			this.GM_deleteValue=GM.deleteValue;
			requires_hdr_str+="@require  https://raw.githubusercontent.com/SloaneFox/code/master/gm4-polyfill.js\n";
		} else check_GM_Support(GM_getValue);
	} catch(e) { this.nonGMmode=true; }; //eg, chrom stadalone

	function check_GM_Support(func) { if (/is not supported[^]{0,100}$/.test( func.toString() ) ) throw "GM functions not supported" ; }

	if (this.nonGMmode){ 
		console.info("WebEraser userscript in non GM mode at "+location.href); //, "typeof GM:",typeof GM, "nonGMmode",nonGMmode,"-- Using local storage.");
		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={};
		}
		this.GM_getValue=function(a,b) { return localStorage[a]||b; };
		this.GM_setValue=function(a,b) { localStorage[a]=b; };
		this.GM_deleteValue=function(a) { delete localStorage[a]; };
		this.GM_listValues=function() { return Object.keys(localStorage); };
		requires_hdr_str+="// @require https://raw.githubusercontent.com/SloaneFox/code/master/gm-popup-menus-1.4.0.js\n";
		this.GM_registerMenuCommand=x=>true; // wait for @require script to define it.
		return true; 
	} else return false;             
}

// No need to load all @requires if script doesn't need them on most pages:

async function dynamicLoadRequires() { // Do delayed network load of js files, alternative to putting in userscript header, js is only loaded when needed.  Example sites: web.whatsapp.com
	var js_ordered_contents, urls=requires_hdr_str.replace(/\n\s*\/\//g,"").split(/@require/).slice(1); // First remove all '\n//', then split and slice 1st off.
	urls=urls.map(str=>str.trim());
	js_ordered_contents=await Promise.all(urls.map(async url=>{  // ensures proper order of files for eval.
		try { 
			console.log("Fetch",url,document.readyState);
			return (await fetch(url)).text(); } 
		catch(e) { 
			// Only upon failure of fetch due to eg, cross sitedness do we need GM_xmlhttpRequest which prompts the user
			// as to allowing the request go through.  On TM the user's choices more elaborate.
			console.error("W/e, failure to fetch",url,e, "Trying GM_xmlhttpRequest");
			var p=pledge();
			GM_xmlhttpRequest({	
				method: "GET",	url: url, onload: function(response) {
					p.resolver(response.responseText);	console.log("xmlHttpRequest res:",response.responseText.substring(0,20));}
			});
			console.log("returning promise",p);
			return p;
		} // fetch needs 2 awaits.
	}));
	js_ordered_contents.forEach((jscript,i)=>{
		console.log("W/e Loading...",urls[i],jscript.length);
		eval(jscript);
	});  // Would need .call to put jscript declaration such as, "var x" in global scope.
		 // However, scoped vars such as unsafeWindow, wrapper puts "var unsafeWindow" but window.unsafeWindow or this.unsafeWindow are undefd.
	dynamic_load_complete=true;
	console.log("Dynamically loaded "+urls.length+" @require files for",script_name);
}

async function getResourceUrl(res_name){ // simply extract url from above resources_hdr_str.
	// if(typeof GM_getResourceURL!="undefined")
	// 	return GM_getResourceURL(res_name);
	// else return GM.getResourceUrl(res_name);
	
	var ar, pos=resources_hdr_str.indexOf(res_name);
	if(pos!=-1)
		return resources_hdr_str.substr(pos).split(/\s+/)[1];
}

ttimer("end globs setup"); //end of outer code whilst main() etc wait for events.

function ttimer(stage,reset) {
//	return; //!! for profiling only.
    if(!ttimer[stage])      ttimer[stage] = { tstamp: (new Date()).getTime() };
	if(ttimer.last_stage)	{
		let nspaces_tab1=40-ttimer.last_stage.length, nspaces_tab2=30-stage.length;

		console.log("Timer: from", ttimer.last_stage + repeat(" ", nspaces_tab1) 
					+ "-------> to "
					+ stage + ":" + repeat(" ", nspaces_tab2), (new Date()).getTime() - ttimer[ttimer.last_stage].tstamp+"ms");		}

	ttimer.last_stage=stage;
	if(reset) {	console.log("Timer Reset!"); ttimer.last_stage=""; }
	function repeat(char,n) { var roll=char; while(--n > 0) roll+=char; return roll;}
}

function pledge() { // creates a promise with its resolver function as a member.
	var resv, rejr, p= new Promise( (r,j)=>{ resv=r; rejr=j; } ); p.resolver=resv; p.rejector=rejr;
	return p; 

	// Use case: 	var p=pledge();	setTimeout(x=>p.resolver(99),1000);	await p;
}

// Performance results:
// url: forms.html

// ----from start: 5ms WebEraser
// 	----to end globs setup WebEraser
// ----from end globs setup: 824ms WebEraser
// 	----to start of main, state: complete WebEraser
// ...
// ----from start of main, state: complete: 624ms WebEraser
// 	----to end of main, state: complete WebEraser

// Run timings on page that has no erasures (TM ff 68.12):
//
// Timer: from start----to end globs setup: 1ms
// Timer: from end globs setup----to main()--start, with readyState: complete: 411ms  (GM: 218ms)
// Timer: from main()--start, with readyState: complete----to main()-inited globs: 8ms (GM: 1063ms)
// Timer: from main()-inited globs----to main()--half.: 9ms (GM 1ms)
//...then on first (shift) click of menu/page:
// Timer: from mainette--start from func:  ----to mainette()--end.: 757ms (GM:439ms)
// For similar on TM, Chromium: 2ms, 307ms,4ms,2ms,81ms.
// On chromium w/o TM: 2,73,2,1,72ms.