slow! / Simple Form Saver

// ==UserScript==
// @name Simple Form Saver
// @version  3.0.6
// @updateURL https://openuserjs.org/meta/slow!/Simple_Form_Saver.meta.js
// @namespace SFS92
// @description Provides a small red button at the top left, click it to save or to fill in forms on the web. Auto-fill, click replay and pasting raw info into forms also featured.
// @license     GPL-3.0-only
// @copyright   2017, slow! (https://openuserjs.org/users/slow!)
// @icon       https://raw.githubusercontent.com/SloaneFox/imgstore/master/sfslogo.gif
// @include  *
// @run-at   document-end
// @require  https://code.jquery.com/jquery-3.2.1.js
// @require  https://code.jquery.com/ui/1.12.1/jquery-ui.js
// @require  https://raw.githubusercontent.com/SloaneFox/code/master/gm4-polyfill-1.0.2.js
// @require  https://raw.githubusercontent.com/SloaneFox/code/master/gm-popup-menus-1.4.1.js
// @require  https://raw.githubusercontent.com/SloaneFox/code/master/sfs-utils-0.1.6.js
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_listValues
// @grant GM_xmlhttpRequest
// @grant GM.registerMenuCommand
// @grant GM.log
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.listValues
// @grant GM.addStyle
// @grant GM.deleteValue
// @grant GM.xmlHttpRequest
// ==/UserScript==
//
// History
//
// updated Nov 2020.   v3.0.2  Change to popup menu and event raising.
// updated Apr 2020.   v2.0.4  Removed coaxInputs from setting of input value.  Also added check to ignore inputs with the "hidden" attribute.
// updated Nov 2019.   v2.0.3  Two lines added for chrome, see quick workaround var: 'tmpStore_loss_of_localStorage.'
// updated Mar 2019.   v2.0.2  Reaction to minor bug reported on chrome initMouseEvent problem.
// updated Jan 2019.   v2.0.1  Removed GM4 transition & GM4 iframe bug.
// updated Dec 2018.   v1.9.10 Removed strange margins fix.  Margins pushing red dot around?
// updated Oct 2018.   v1.9.9 Provided new function "append" to allow the filling of many forms at same url address.
// updated Feb 2018.   v1.9.6 To allow run under GM4 and backwardly to prior versions of GM.
// updated Dec 2017.   v1.9.1 Added new menu command to manually set active input value
// updated Dec 2016.   v1.9.0 Added user profiles which allows multiple stores of form data on same page.  Removed complex chrome adapter, removed address book functions. Uses github due to traffic at openuserjs.
// updated Sept 2013.  v1.6.3 Can add further clicks to page
// updated Sept 2013.  v1.5.1 Address/Notebook (alt-a), unhide password & Export/Import added.
// updated Sept 2013.  v1.5.0 Updated to coax dynamic forms to work.
// updated for firefox 22.0
// updated March 2013.  Fixed Chrome issue with recording a click.  v.1.3.2
// updated March 2013.  Restricted regexp page matching. v.1.3.1
// updated March 2013.  Dialog window in new version of browser was tiny so text was unviewable, using own versions of dialog boxes, alert, confirm and prompt.  v.1.3.0
// updated February 2011.  See userscripts.org description page for this script regarding change in tick box sizing.  Changes only by request.
// updated January 2011.  Updated for Greasemonkey 0.9, javascript's basic function, eval(),  no longer works fully in GM.
// updated January 2011.  Updated for Firefox 4.
// updated December 2010.  Allows editing of auto-click target element.
// updated 17 October 2010.  Google Chrome platform adaptation.
// updated 7 October 2010.  Added feature allowing data to be pasted directly into a form.  See last part of description below.
// updated 1st of March 2010.  Auto delete an element on a page, select record mouse click and do control-shift-click on the element.  Remove iframes.
// updated 5th of February 2010.  Can fill in pop-ups and other elements that appear during use of a page.
// updated 29th of Janaury 2010.  Facility for editing form storage manually.
// updated 27th of Janaury 2010.  Problem with duplicate input names solved.
// updated 25th of Janaury 2010.  Handling of input name changes and multiline textareas added.
// updated 18th of Janaury 2010.  Added ability to record a mouse click on a page with autoplay.
// updated 16th of Janaury 2010.  Enable selective fill-in of forms on user selected pages.
// updated 16th of Janaury 2010.  Enable iframes with forms to work, and forms that auto update on change.
//
// @run-at   document-start //problem, if at start, GM_registerMenuCommand does nothing.
// icon was https://bit.ly/1Qre8Je
// Data other than settings etc. is stored as GM get/setValues named: [current profile]hostFormsList and formsAddressBook.
//
//

// 'this' is not window in firefox but Sandbox.
ttimer("Start");
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/gm4-polyfill-1.0.2.js
// @require  https://raw.githubusercontent.com/SloaneFox/code/master/gm-popup-menus-1.4.1.js
// @require  https://raw.githubusercontent.com/SloaneFox/code/master/sfs-utils-0.1.6.js
`

requires_hdr_str="";

function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}
var iframe=window.parent != window;

loader();
async function loader() {
	await chromeInit();
	if(iframe) { await sleep(3000);}
	if(document.readyState=="complete") main();
	else addEventListener("load",main);
}

//function log() {	console.log.apply(console,arguments);};
//log=x=>null;  // original from sfs-utils.js

// console.log("Form Saver, start.  ",iframe?"In iframe":"Not in iframe","\n\t\tLocation:",location,"this:",this,this==window);

//
// Globals
//

var uwin=unsafeWindow; uwin.sfs_reddot_running=true;
var inputs_on_page=0, text_inputs=0, no_of_inputs=0, special_ta;
var wdoc=window.document;
var selects=wdoc.getElementsByTagName("select"), tareas=wdoc.getElementsByTagName("textarea"), rinputs=wdoc.getElementsByTagName("input");
var inputs=[], input_names={}, old_elems=[];
var automatic=false;
var reddot=true, suspend_replay=true, full_icon=false, element_to_highlight, logos=[];
var hash_host_list = new Object();
var page_key, site, page_object, regexp_page_match, href, form_data, click, recording;
var img, img2, link;
var pasteWindow;
var auto_fill=false, no_element_for_click=true, prev_msg, msg, unhide, init_fin, wait_for_click;
var ws=window.setTimeout, write_once=true; //noscript can block timeout.
var widener="\n___________________________________________________\n";
var bullet="\u26ab", bullet_tab="\u26ab\t", tab_bullet="\t\u26ab",separator=":\u200e:", mdash_char = "\u2014",
	bullet_regexp=/\s*\u26ab\s*/g, 
	tick = "\u2714", ex="\u2717"; //  bullet_regexp=RegExp("\s*\u26ab\s*", "g"); 
var data_version;
var dynamic_load_complete, waiting_for_an_option;
var addedByLine="\nAdded by userscript, Simple Form Saver.", userProfileLine;
var stringify=JSON.stringify, parse=JSON.parse;
var jQinClosure=typeof $!="undefined"?$:null;   //Closure used since jQuery in window object can be corrupted when other jq modules not loaded.
var triggerEvents=true; //false; //true; //false; //experimental simulation.

// Got error  when triggering events from simulkeyhit()
//Error: "Permission denied to access property "which""
//    isValidKey https://.na/ri-common/javascript//common.js?version=4744db25675f4545c88af30f784f88add3effc87:106
//    onkeypress https://.na/inet/ri/login.htm:
// Need to put in unsafeWindow.

log2=function(){};

async function setValue(n,v) { console.log("setValue",n,v); return await GM_setValue(n,stringify(v));}

async function getValue(n,v) { 
	if (v===undefined) v=null;
	var raw=await GM_getValue(n,stringify(v));  // use of GM_ since this is used prior dynamic load.
	if(raw=="undefined") raw=stringify(v);
	return parse(raw);
}

var hostFormsList="hostFormsList"; // default, is set to: profiles.current+"hostFormsList";
var profiles, otherProfiles, updated_GM4_value, globs_inited;

function mutexlock() { this.lock=new Promise(r=>this.unlock=r);};
var init_globs_mutex=new mutexlock();

async function init_globs(){try{
	if(globs_inited) { await init_globs_mutex.lock; return; }
	else globs_inited=true;
	await dynamicLoadRequires();          ////// @require js files loaded.  \\\\\\\\\\
	log=x=>null;
	//GMDataEditor();
	profiles=new profile_bridge();
	await profiles.init();                   // sets hostFormsList name.
	await readPersistentData("init_globs");  // uses hostFormsList name.  Checks for inputs.
	await init_configVars(); // awaits all sub-awaits to finish.
	userProfileLine=profiles.current ? "\nUser profile: "+profiles.current+ "\t(non-default->icon decor)" : "";
	otherProfiles=await profiles.checkOtherProfiles();
	init_globs_mutex.unlock();
}catch(e) { console.error("Error in init_globs",e);}}

var gLine_order,gOptionals,gParse_method, gPaste_shortcut;
async function init_configVars() {try{
	gLine_order=await getValue('line_order', "");
	gOptionals=await getValue('optionals', "");
	gParse_method=await getValue('parse_method', "values");
	gPaste_shortcut=await getValue('paste_shortcut', false);
}catch(e) { console.error("Error init configs",e);}}


//console.log("userProfile:"+profiles.current+".","profiles_list:"+profiles+".","hostFormsList:"+hostFormsList+".");

String.prototype.indexOfRegExp = function(regex, nomatch){ var match = this.match(regex); if (match) return this.indexOf(match[0]); else return (nomatch==undefined ? -1 : nomatch);};
//String.prototype.trim = function () {    return this.replace(/^\s*|\s*$/g,""); }; //all space chars from wither end.
String.prototype.splitOnce = function (r) {   r=RegExp(r.source||r, "g"); var a=r.exec(this); return [ this.substring(0, a?a.index:""), this.substr(r.lastIndex) ] ; };
String.prototype.tail = function (n) {  var last_pos = this.length - n; if (last_pos < 0) last_pos=0;  return ( n < this.length ? "..." : "" )+this.substr(this.length-n); };
Date.prototype.slashFormat=function(){var dd=this.getDate();if(dd<10)dd='0'+dd;var mm=this.getMonth()+1;if(mm<10)mm='0'+mm;var yyyy=this.getFullYear();return String(mm+"\/"+dd+"\/"+yyyy)};
Date.prototype.ampmFormat=function() { var ap = "am", hour=this.getHours(), mins=this.getMinutes(); if (hour>11) ap = "pm";  if (hour   > 12)  hour = hour - 12; if (hour   == 0)  hour = 12; return hour+":"+(mins<10?"0"+mins:mins)+" "+ap;};



//
// Mainline of sfs
//
async function main() { try {
	ttimer("main()-start");
	await init_globs(); //////////////////
	ttimer("main()-inited data");
//	GM_addStyle(await jqueryui_dialog_css(),"sfs-dialog-style");
	GM_addStyle(jqueryui_dialog_css); //,"sfs-dialog-style");
	wdoc=window.document;
	// init data not there at document-start
	selects=wdoc.getElementsByTagName("select"), tareas=wdoc.getElementsByTagName("textarea"), 
	rinputs=wdoc.getElementsByTagName("input");
	await submenuModule.register("Simple Form Saver",null,"#cccccc","#002a00");        // Sets GM_registerMenuCommand()    "#103010");// title col.,bg color.// js may fail to load.
	//this.submenuModule.register("Simple Form Saver",null,"#3f005e","#ffffee");
	ttimer("main()-mid");
	if (hash_host_list.msg || hash_host_list.prev_msg) {
		msg=hash_host_list.msg;
		prev_msg=hash_host_list.prev_msg;
		if ( ! checkIfIntervalLongEnough() && msg) 
			window.status="Form saver msg:   "+(msg);
		hash_host_list.prev_msg=msg;
		prev_msg=msg;
		if ( ! msg) delete hash_host_list.prev_msg;
		delete hash_host_list.msg;
		persistData();
	}
	if(iframe) window.parent.postMessage({type:"break-iframe-sfs",stage:"madeit"},"*");
	if (inputs.length==0 && ! (form_data||click)) {
	    //!! console.info("No inputs --> No SFS reddot.");
	    registerMenus();
	    //!! return;  // would prevent addicon below
	}
	if (getXPathElem())
		no_element_for_click=false;
	if ( (page_object.automatic || click) && ! suspend_replay) {      // suspend_replay must override page local setting, otherwise user cannot get a chance to change sfs settings.
		addIcon(true); //will register menus in setLive()
		console.log("Check interval then fill or replay click, auto:",page_object.automatic,"click",click,"sr:",suspend_replay);
		if (checkIfIntervalLongEnough()) {
			auto_fill=true;
			if (page_object.automatic !== false
				&& (page_object.automatic || ! no_element_for_click)
				&& ! regexp_page_match) {
				fillForm(false, true);
			}
			console.log("interval passed, Filled form now replay click."+click);
			setTimeout(replayClick,3000); // !! delay needed?
			inOutSet();
			auto_fill=false;
		}
	}
	if (page_object.click_evidence) 
		delete page_object.click_evidence.sealed;
	
	if ( ! link )
		addIcon(true); //will register menus
	if (/Simple Form Saver replayed/.test(msg)) {
		inOutSet(true);
	}   
	//keepEyeOnIcon();
	//log("Set icons.");

	registerMenus();
	if (click && suspend_replay) {                        // && ! page_object.automatic===true    ) { 
        console.log("Suppressed replay on newly loaded page, auto-replay is suspended, to replay click icon."); 
    }
	window.onbeforeunload=function(e) { 
		if (recording) { 
			var dialog=$("#sfsconfirm3"), openeddialogs;
			if (dialog.length && dialog.parent().css("display")!="none") 
				openeddialogs=true;

			//log("onbeforeunload "+openeddialogs+" "+e.target.tagName+" cc "+wait_for_click+" "+uneval(e)+" act:"+document.activeElement.tagName);
			var roll=""; for (i in e) roll+=i+" "+e[i]+"\n";
			var pn=e.explicitOriginalTarget.parentNode;
			//log(" "+e.eventPhase+" "+e.originalTarget+" "+(pn?pn.tagName+" ":"")+e.timeStamp);
			var pev={};
			pev.target=e.explicitOriginalTarget.parentNode;
			if (wait_for_click) setTimeout(function() { log("call cl"); recordClick(pev);}, 100);
			if(wait_for_click || openeddialogs) {
				var p=prompt_interruption, note="No dialogs open"; //!!! for jdialog???
				if (prompt_interruption) { sprompt(p.a,p.b,p.c); }
				log("block exit, call confirm");
				confirm(dialog.parent().css("display")+".  The page on which you are recording has in fact detected the click and wishes to unload.  "
						+"However, you must complete interaction with the recording dialogs beforehand."
						+"\n\nPlease finish with recording dialogue in other window and ONLY then click OK or Cancel here in this window.  A window may flash up momentarily, just ignore it.  "
					   );
				interrupted=true;
			}
		}//end if recording
	}; //end beforeunload
	//console.log("done.",window.onbeforeunload, ", pagewin:",unsafeWindow.onbeforeunload);
	window.addEventListener("unload", function(){ try{
		//readPersistentData();
		var click_evidence; try { click_evidence=parse(sessionStorage.click_evidence||0);} catch(e){console.log("old json click_evidence");}
		//console.log("unload, iframe",iframe,"stack",(new Error).stack);
		sessionStorage.removeItem("click_evidence");
		page_object.click_evidence=click_evidence;
		if (page_object.click_evidence) {
			if ( page_object.click_evidence.prejudice) 
				delete page_object.click_evidence;
			else
				page_object.click_evidence.sealed=true;
			sessionStorage.click_evidence=stringify(page_object.click_evidence);
			//log("unload ",page_object.click_evidence,page_object.click_evidence.prejudice);
			//persistData();
		}
		if (pasteWindow && !pasteWindow.closed) pasteWindow.close();
	}catch(e){console.log("SFS, Unload event error",e,"Perhaps not allowed access from location:",location);}
												}, false); //end unload
	
	if (click || page_object.automatic) {
		uwin.addEventListener("DOMNodeInserted", handlePopups, false);
	}
	if (gPaste_shortcut) {
		addEventListener("keypress", function(e) {
			if (e.charCode==118 && e.ctrlKey) //118 is v
				if ( ! /INPUT|TEXTAREA/.test(e.target.tagName)) {
					special_ta=document.createElement("textarea");
					special_ta.id="speciality";
					document.body.appendChild(special_ta);
					//special_ta.focus();
					e.preventDefault();   
					e.stopPropagation();
					setTimeout(function() {
						log("tout"); 
						pasteIntoForm();
						document.body.removeChild(special_ta);
						special_ta=null;
					}, 2);
				}
			return true;
		},0);
	}
	init_fin=true;
	ttimer("main()-end");
} catch(e) {console.error("Error in simple_form_saver main():",e);} // re skips long file path
} //main()

function saveFormData(nosave,append) {  try {
	var output_string = "";
	var  i, plicate={}, saved_data={}, nothing=true;
	if (regexp_page_match && page_object.form_data && !nosave) saved_data=page_object.form_data;
	if (append) saved_data=page_object.form_data;
	//log("save data, beg "+inputs.length);
	for(i=0; i < inputs.length; i++) {
		var input=inputs[i];
		var value=input.value;
		var type=input.type;
		if ( value == ""  && /text|textarea/.test(type))
			continue;
		nothing=false;
		var name=input.name;
		if ( ! name) name="yyNoName"+i;
		var checked=input.checked;
		value = value.replace(/\r\n/g,"\u200d");
		value = value.replace(/\n/g,"\u200d");
		if (type=="checkbox") {
			name+=mdash_char+value;
			if (checked) 
				value=tick;
			else
				value=ex;
		}
		if ( ! plicate[name]) 
			plicate[name]=[];
		plicate[name].push(value); //save name and value in case more than one input with same name.
		
		if ( type == "radio")
			if ( ! checked ) {
				plicate[name].pop();
				continue;
			}
		if (  type  ==   "select-multiple"  ) { 
			var options_list = [];
			for ( var index=0; index < input.options.length; index++ )
				if ( input.options.item(index).selected )
					options_list.push(index);
			value = options_list;
		}
		if (  type  ==   "select-one"  ) {
			if (input.selectedIndex==-1) continue;
			for ( var index=0; index < input.options.length; index++ )
				if ( input.options.item(index).selected )
					value=index;
			if (value.substr)
				value=input.selectedIndex;
		}	    
		//log("Save On  i:"+i+".  Elem_name: "+name+".   type: "+type+".  elem_val:"+value+"."+", typeof value: "+typeof value+", ply: "+plicate[name].length);
		saved_data[name]={};
		saved_data[name].v=value;
		saved_data[name].i=i;
		if (type=="password") {
			saved_data[name].pw=1;
		}
		if (plicate[name].length > 1)
			saved_data[name].p=plicate[name];//.slice();
		else
			delete saved_data[name].p;
		//log("Saved Data, val: "+saved_data[name].v+".   i: "+saved_data[name].i);
	} // end for
	var request=true;
	if ( ! nothing || nosave) {
		if (iframe && ! regexp_page_match) 
			sprompt("***Simple Form Saver***\n\nPage stored: "+page_key+widener+"This form is part of a subpage (an iframe) within the current window.  "
					+"It is controlled via the icon, or via shortcuts, whereas any forms not in the subpage but in the main window are controlled from the GM menu."
					+"\n\nYou can get status information via the icon or GM menu and check to which page it is refers (first line of the status information window) "
					+"\n\nCancel and change the page name below or enter a pattern for which the form data shall be stored.  Any page matching this pattern can then be used for form filling.  Use just the site name for it to be in effect for entire site."
					+"\n\nHit OK to save as usual for the page: "+page_key
					, page_key
					,function(reply) {
						
						if (reply && reply != page_key) {
							delete hash_host_list[page_key];
							page_key=reply
							page_object.form_data=saved_data;
							log("po "+page_object+"cnt "+ countMembers(page_object));
							persistData();
							window.status="Forms saved for this page"
								+", # input fields stored: "+i+".  At: "+page_key;
						}
					}); //end function(reply) 
		page_object.form_data=saved_data;
		//log("po "+page_object+"cnt "+ countMembers(page_object));
		persistData();
		window.status="Forms saved for this page"
			+", # input fields stored: "+i+".  At: "+page_key;
	} //endif ! nothing || nosave
	else {
		if (!nosave)
			window.status="Nothing on forms to save";
		else return saved_data;
	}
}  catch(e) {alert("Cannot save form, reload and try again.\n\nError was:"+e+" "+e.lineNumber);throw(e);}
								 log("win.status "+window.status+", fd: "+form_data+", po.fd: "+ page_object.form_data+", sd: "+saved_data);
							  }; //end saveFormData())

function fillForm(once_off, auto, tmp_data) {  try { //also Click replay if necessary
	//	log("fillForm(once_off: "+once_off+", auto: "+ auto+", tmp: "+uneval(tmp_data));
	var saved_data= tmp_data ? tmp_data : form_data;
	var name, saved_obj, elem_name = "";
	var changed=null, kick, result=true, fill_msg;
	var type, value,  indeterminate, plicate={};
	var input, option, scrollPos=$(window).scrollTop();
	//function setMember(obj, member, value) {
	bodymsg("fillForm()");
	function setMember(member, value) {
		if ( member != value) changed=input.name||true;
		if(changed) log("value changed from:"+member+", to:"+value+".");
		return;
		/* if (eval(obj+"."+member) != value) { */
		/*   changed=true; */
		/*   var js=obj+"."+member+"='"+value+"'"; */
		/*   if (typeof value=="boolean") */
		/*     js=obj+"."+member+"="+value; */
		/*   eval(js); */
		//}
	}
	var plicate={},ultimate;
	var finputs=inputs, jip;
	if (once_off) finputs=once_off;

	for(var i=0; i<finputs.length; i++) {            /////// Main loop over each field
		input=finputs[i]; this.input=input;
		name = elem_name = input.name;
		type=input.type;
		elem_value=input.value;
		value=undefined;
		changed=false;
		jip=$(input);
		triggerStartEventSeq(jip);
		
		//fakeClick(input);
		//log2("Fill, On  i:"+i+".  Elem_name: "+elem_name+".   type: "+type+".  current elem_val: "+elem_value);
		if ( ! elem_name) name="yyNoName"+i;
		if (i==finputs.length-1) ultimate=true; 
		if (type=="checkbox")
			name=elem_name+mdash_char+elem_value;
		saved_obj=saved_data && saved_data[name];
		//log2("saved_data["+name+"]="+(saved_obj ? ".v="+saved_obj.v+". (datatype:"+(typeof saved_obj.v)+"), .i= "+saved_obj.i + ".  .p= "+saved_obj.p : " null"));

		if (saved_obj && (saved_obj.i != undefined) && saved_obj.i  != i  && ! once_off) { 
			if (finputs[saved_obj.i] && finputs[saved_obj.i].name == elem_name) {
				indeterminate=true;
			}
			else {
				saved_obj.i=i; 
			}
		}
		if (saved_obj && saved_obj.p ) {
			//log("plicate "+saved_obj.p);
			if ( ! plicate[name] && saved_data[name].p) {
				if (!saved_data[name].p.slice)
					saved_data[name].p = convert_obj_to_array(saved_data[name].p);
				plicate[name]=saved_data[name].p.slice(); // copy 
				plicate[name].pop();  // top of stack is also the one in form_data.
			}
			value=plicate[name].shift();
			if (value && indeterminate)
				indeterminate=false;
		}
		if ( ! value && saved_obj && saved_obj.v) value=saved_obj.v;
		//log("value: "+value+", indeterminate "+indeterminate);
		if (indeterminate) { indeterminate=false; value=undefined;} // i and saved_obj.i don't match
		if ( value === undefined && ! once_off && type !="radio") {
			var res=findRenamedInput(name, i);  //checks for value in saved .i index
			value=res[0];
			if ( value) { 
				fill_msg="The "+ordinal(i+1)+' input field, "'+name+'", may have been renamed, try re-saving form information if it is not an automatic renaming by the server.  ';
				//log(fill_msg+"It was filled in with the previously saved value, " 
				// 		+value+", for the "+ordinal(i+1)+" field,"+ordinal(res[2])+","+res[1]+".  Page: "+page_key);
			}
		}
		if (value===undefined) {
			if (type == "radio" ) {
				// 	input.value="";
				value=saved_obj?saved_obj.v:"";
			}
			else { 
				if (type=="checkbox" ) {
					if (input.checked)
						changed=true;
					input.checked=false;
				}
				//log("Skip "+i);
				continue;
			}
		} //end if undefined
		//log("Check type, value: "+value+", type: "+type);
		if (value && typeof value == "string") value = value.replace(/\u200d/g, "\r\n");
		if (type=="checkbox") {
			if (value== tick) 
			{ setMember(input.checked, true); input.checked=true;}
			else
			{ setMember(input.checked, false); input.checked=false;}
		} 
		else if ((type == "radio") && elem_value == value)
		{setMember(input.checked, true); input.checked=true}
		else if (type  !=  "radio") {
			if (finputs.tagName=="TEXTAREA") {
				if (value != elem_value)
					changed=true;
			}
			else 
				if (type  ==  "select-multiple" )   { 
					options_list=value;
					for (let index=0; index < input.options.length; index++) {
						if (input.options.length <= index ) { result=false; continue }
						if ( RegExp ("\\b"+index+"\\b").test(options_list)   )  
						{ setMember(input.options.item(index).selected, true); input.options.item(index).selected=true;}
						else
						{ setMember(input.options.item(index).selected, false); input.options.item(index).selected=false;}
					}
				}
			else if (  type  ==   "select-one"  ) { 
				//log2(" Select, saved index "+value+".  Current selected Index: "+input.selectedIndex+" of "+input.options.length+".  .value "+input.value);
				if (input.options.length <= value) {  result=false; continue }
				var index, set_select;
				try { if (input.selectedIndex!=value) {changed=true;input.selectedIndex=value; } } catch(e){}
				for ( index=0; index < input.options.length; index++ )
					if ( index == value) {
						setMember(input.options.item(index).selected, true); 
						input.options.item(index).selected=true;
						option=input.options.item(index);
						//fakeClick(input.options.item(index));
					}
			}
			else {  //simple input
				//log("set member input",input,"to",value);
				setMember(input.value, value); 
				// if (value.slice) input.value=value.slice(0,value.length/2);
				// else 
				input.value=value;
				//getAnonymousNodes The getAnonymousNodes method retrieves the anonymous children of the specified element.
				//only visible in dom inspector (red in color, beneath INPUTs at first as DIV with a BR then when filled as DIV with direct text.
				// setTimeout(function(sinput, svalue) {      //pass parameters to it from stack
				// 	//log("setTimeout Filler set:"+sinput.name+"="+svalue+"."); 
				// 	sinput.value=svalue;
				// 	coaxInputs(sinput, svalue);
				// }, 50, input, value); // input, value parameters passed to setTimeout function stack, 50 is timeout.  To add to eventlistener extra param needed.
				//input.value=value;
				//log("last setMember "+value);
			}
		} //end if type != radio
		//log("Set value of input["+i+"].type: "+type,input,", saved value is:"+value + ".  Current input.value: "+input.value+ ". seloption: "+input.selectedIndex +".  Changed: "+changed+".");
		if (changed) {  // trick to simulate event if (changed && false) !!
			kick=changed;
			// changed=false;
			let evobj={ keyCode: 65, which:65, key:"a", charCode:0 }, 
 				evobj2={ keyCode: 8, which:8, key:"Backspace", charCode:0 };;
			simulkeyhit(jip,evobj,evobj2);
			triggerEventSeq(jip,"change");
			triggerEventSeq(jip,"blur");
			triggerEndEventSeq(jip);
			// var pseudo_event = window.document.createEvent("MouseEvents");// create event
			// pseudo_event.initMouseEvent("change", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
			// pseudo_event = window.document.createEvent("MouseEvents");// create event
			//fakeClick(input);
			//if (option) { fakeClick(option);option=null;}
		}


		
	} // end Main loop over fields/inputs
    setTimeout(x=>$(window).scrollTop(scrollPos),0); //restores

	var write_msg=(fill_msg?fill_msg+" ":"")+"Filled in forms"
		+(once_off ? " for dynamic element on "+page_key : " at "+page_key )+"."+userProfileLine
		+( ! result? "Bounds error.  " : "") +( kick ? " Changes were made.  " : "No changes were necessary.  ")
		+(fill_msg ? fill_msg : "");
	//window.status=write_msg;
	bodymsg(write_msg);
	console.info(write_msg);
	old_elems=inputs.slice(0);
	if ( result && ! once_off && ! auto ) {
		//log("Filled form, replay from filler as manual ");
		//replayClick(true);
	}
	return result;
} catch(e) { console.log(e);alert("Cannot fill-out form, reload and try again.\n\nError was: "+e+", line# "+e.lineNumber); throw(e);}

function bodymsg(str) {
    return; // !! changed element to click's path.
	var body=$("body"),div=$(".fill-Form");
	if(!bodymsg.setup) {
		bodymsg.setup=true;
		div=$("<div class=fill-Form style=display:none>");
		body.prepend(div);
	}
	div.text(div.text()+"\n\n"+(new Date)+"\n"+str);
}

function triggerStartEventSeq(el) {
	triggerEventSeq(el,null,"start");
}
function triggerEndEventSeq(el) { 
	triggerEventSeq(el,null,"end");
}

function simulkeyhit(el,evobj,evobj2) {
	if(!triggerEvents) return;
	evobj.isTrusted=true; evobj.originalEvent={isPrimary:true}; evobj.preventDefault=x=>x;
	el.trigger($.Event( "keydown",  evobj));
	el.trigger($.Event( "keypress", evobj));
	el.trigger($.Event( "keyup", evobj ));
	if(evobj2) simulkeyhit(el,evobj2);
}		
} // end fillForm();

function triggerEventSeq(el,otherEvent,stage) { // default is click event (ie, mdown, focus, mup, etc.)
	if(!triggerEvents) return;
	//console.log("Trigger Event",el,otherEvent,stage);
	var oevent=evName=> {
		var e = $.Event(evName,{isTrusted:true, isPrimary:true});
		el.trigger(e); //jip.click();
	};
	if(!otherEvent) { 
		if(stage=="start"){
			oevent("mousedown"); oevent("focus"); oevent("mouseup"); oevent("click");
			oevent("compositionStart"); oevent("input");
		} else if(stage=="click"){ 
			oevent("mousedown"); oevent("focus"); oevent("mouseup"); oevent("click"); 
		}
		else{
			oevent("input");
			oevent("compositionEnd");
			oevent("blur");
		}
	}
	else oevent(otherEvent);
} // end triggerEventSeq()

function profileChange(){
	var prof_ar=profiles.toArray(), curr_prof=profiles.current;
	var msg="Give a new name to create a profile, use an existing profile name, or blank to use default base profile"+ (curr_prof?" (current profile is:"+curr_prof+")" : "" ) +".";
	if (prof_ar.length) msg+="  Available profiles (clickable): "
		+("<a class=profs_c>base</a>, "+prof_ar.reduce((prev,current)=>prev+"<a class=profs_c>"+current+"</a>, ","")).replace(/,\s*$/, "")+"."
		+(otherProfiles.length ? "\n\nProfiles, other than base and current one, that have prior stored values: " + otherProfiles +".": "");
	msg=new String(msg);msg.html=true;
	var dialog=sprompt(msg,"",async function(reply) {
		if (reply!=null) {
			reply=reply.trim();
			if (reply[0] != "-") {
				if(reply.includes(" ")) { alert("Spaces are not allowed within profile names."); return; }
				profiles.current=reply;

			} // endif reply[0] != -
			else { // delete given profile from list.
				reply=reply.substr(1).trim();
				if (await profiles.rm(reply)==-1) alert("No such profile to delete.");
			}
		} // endif reply != null
		
	}); // end sprompt()
	dialog.find(".ui-dialog-content")
		.attr("title","Set the current user profile, create a new profile name (it becomes the current profile automatically), or delete an existing one.\nThe special 'base profile' is the default and has no name."
			  +"\nPut a minus sign (-) before a profile name to delete it.\nEnter a name in the box below or click one of the exising profile names."
			  +"\n\nProfiles allow multiple sets of form data to be stored for the same page, for example, if a form is often filled in on behalf of another "
			  +"person and you also wish to keep differing form info for yourself or others.  Once a profile is current, Simple Form Saver will persistently use that profile session until it is explicity changed or is reverted to the base (default) profile.  Old tabs may need to be reloaded to see the changes made here.");
	dialog.find("a.profs_c").click(e=>{ 
		dialog.trigger(jQuery.Event("keydown",{keyCode:27,key:"Escape"})); //close prompt.
		profiles.current=e.target.textContent.replace(/^base$/,""); // Set profile selected.  BTW, this is el to which handler was attached.
		return false;
	});
	//dialog.find("input").focus();
	//setTimeout(function(){dialog.find("input").focus();},200);
}

function profile_bridge() { // abstract storage (accessors) for back compatibility.  Used at top in: var profiles=new profile_bridge();
	var currentProfile, full_list, list, update;
	this.init=async function() {
		if(this.inited) return; else this.inited=true;
		currentProfile=await getValue("currentProfile", "");   //
		full_list=await getValue("profiles_list",[]); // Parallel arrays, full list and list (of strings).      GM_deleteValue("profiles_list");GM_deleteValue("currentProfile");
		list=full_list.map(el=>el.name);        
		//console.log("set profiles_list list to",list);
		update=async function() { list=full_list.map(el=>el.name); await setValue("profiles_list",full_list); await setValue("currentProfile", currentProfile); }; 
		var accessors={
			get:function(){ return currentProfile.title||"";},    // "" if not using profiles.
			set:async function(newprof_name) {                           // Setting to a new name creates a new profile.  Change profile.
				if (!newprof_name) currentProfile="";
				else {
					if ( ! list.includes(newprof_name)) 
						full_list.push({name:newprof_name});
					currentProfile={title:newprof_name};
					$("#SFSimg").css({border:"dotted 5px"}); //similar in setLive if not base prof.
				}
				console.info("Simple Form Saver, changing profile to "+(this.current||"base profile")+".");
				submenuModule.changeName("Profile Change.*",
										 "Profile Change ["+(profiles.current?profiles.current:"base profile")+"].");
				await update(); //Does storage of values
				hostFormsList=this.current+"hostFormsList";
				await readPersistentData("profile_bridge");
				userProfileLine=this.current ? "\nUser profile: "+this.current+" (nondefault -> icon decor)" : "";
				setLive();
			}
		}; //end accessors
		Object.defineProperty(this,"current", accessors); // get & set for attrib "current".
		hostFormsList=this.current+"hostFormsList";
	}; //this.init()
	this.toString=function(){ return list.toString(); };
	this.toArray=function(){ return list; };
	this.rm=async function(profile_name) {
		if (!profile_name || !list.includes(profile_name)) return -1;
		full_list.splice(list.indexOf(profile_name),1);
		if (this.current==profile_name) {
			page_object={}; persistData();
			this.current="";
		}
		else { let old_prof=this.current; this.current=profile_name; page_object={}; await persistData(); this.current=old_prof; }
		update();
	};
	this.checkOtherProfiles=async function() {//try{
		// for each profile check if form_data or click set.
		var curr_prof=this.current, original_hfl=hostFormsList;
		// The following was list.filter but since it is async changed to map & used Promise.all(list)
		//log("checkOtherProfiles list:",list);
		var profiles_with_values=list.map(async x=>{
			if (x==curr_prof) return;
			hostFormsList=x+"hostFormsList";
			await readPersistentData("coprofs");
			if (form_data || click) return x;
		});
		var filtered=Promise.all(profiles_with_values);
		profiles_with_values=(await filtered).filter(x=>x);
		hostFormsList=original_hfl;
		await readPersistentData("coprofs2");
		return profiles_with_values;
	//}  catch(e){console.log("checkOtherProfiles error,",e.lineNumber,e);}
}
	
} //profile_bridge()

function coaxInputs(input, value) {
	//Live dynamic inputs need coaxing, test in 1.5.0:
	var act_el=window.document.activeElement;
	//log("coaxing ",input,+input.name+" "+value," active el:",act_el);
	input.focus();         try{
		moveCaretToEnd(input);    }catch(e){}
	var evt = document.createEvent("KeyboardEvent"), evt2=document.createEvent("KeyboardEvent"), evt3=document.createEvent("KeyboardEvent"), evt4=document.createEvent("KeyboardEvent");
	//evt.initKeyEvent("keypress", true, true, null, "U+0008", 0, "");
	var keyval=98; // 98 'a', 8 is backspace
	if (!plat_chrome) {
		jQuery.event.trigger({ type : "keypress", which : keyval});
		evt.initKeyEvent ("keydown", true, true, window,   0, true,    false,  false,  keyval,            0); 
		evt2.initKeyEvent ("keypress", true, true, window,   0, true,    false,  false,  keyval,            0); 
		evt3.initKeyEvent ("keyup", true, true,  window,   0, true,    false,  false,  keyval,            0);
		evt4.initKeyEvent ("textinput", true, true,  window,   0, true,    false,  false,  keyval,            0); 
	}
	else {
		evt.initKeyboardEvent ("keydown", true, true, window,   0, true,    false,  false,  keyval,            0); 
		evt2.initKeyboardEvent ("keyup", true, true,  window,   0, true,    false,  false,  keyval,            0); 
		evt3.initKeyboardEvent ("keypress", true, true, window,   0, true,    false,  false,  keyval,            0);
		evt4.initKeyboardEvent ("textinput", true, true,  window,   0, true,    false,  false,  keyval,            0); 
	}
	input.dispatchEvent(evt);
	input.dispatchEvent(evt2);
	input.dispatchEvent(evt3);
	input.dispatchEvent(evt4);

	act_el.focus();
}

function moveCaretToEnd(el) {
	el.focus();
	if (typeof el.selectionStart == "number") {
		el.selectionStart = el.selectionEnd = el.value.length;
	} else if (typeof el.createTextRange != "undefined") {
		var range = el.createTextRange();
		range.collapse(false);
		range.select();
	}
}

function registerMenus(full_menus) {
	//
	// GM menus
	//
	var that=registerMenus;

	if(full_menus) registerPageMenus(); 
	registerGeneralMenus();
	
	function registerGeneralMenus() {
		if (that.gendone) return;
		that.gendone=true;
		registerMenuCommand( "========Simple Form Saver======", function(){});
		registerMenuCommand( "Save Form",
								async function() {
									await readPersistentData("Save Form");
									if ( inputs.length==0)
										return;
									useCorrect_page_object();
									saveFormData();
									if ( ! link )
										addIcon(true);
									setLive();
									inOutSet();
								});
		
		window.openFormStatus=async function(cb) { 
			await readPersistentData("status"); 
			console.log("Opening form status, log is:",log);
			msg=new String(getStatusInfo());msg.html=true;
			var prompt=salert(msg,cb);
			prompt.find("a.submenu_c").click(e=>{   
				prompt.trigger(jQuery.Event("keydown",{keyCode:27,key:"Escape"})); // jQ will call close on prompt.
				submenuModule.open(); // To open another script's menu: $("[script-name^='Simple Form Saver']").parent().addBack().css("display","");
				return false;
			});
			if (element_to_highlight) element_to_highlight.style.borderStyle="none";
			return prompt;
		};
		registerMenuCommand( "Get Form Status & Settings",window.openFormStatus
								, "","","G");

		registerMenuCommand("Profile Change ["+(profiles.current?profiles.current:"base profile")+"].", profileChange,"","","P");
		
		registerMenuCommand( "Record next mouse click on document...",
								async function() { 
									log("ask confirm "+!plat_chrome);
									await readPersistentData();
									if (click) { 
										sconfirm("A click has been previously recorded, hit OK to delete it and re-record, or Cancel to make addition clicks or abort", function(reply) { 
											if (!reply) {
												log("Reply:"+reply+"."+typeof reply+" "+(!reply));
												sprompt("Please enter the path (XPath) for the additional item to be clicked, eg, /html/body/div[1]/div, for XPath see inspection console and choose 'copy->xpath' or see element highlighting when recording a first click.","",
														function(reply){ 
															if (reply==null) { console.log("No extra click recorded");return; }
															if (!click.further_clicks) click.further_clicks=[];
															click.further_clicks.push(reply.trim());
															var xpath=click.further_clicks[click.further_clicks.length-1];
															var celem=uwin.document.evaluate(xpath,uwin.document,null,6,null).snapshotItem(0);
															//console.log("Extra click is on:"+celem.tagName+", "+celem.textContent);

															persistData();
														});
												return;
											}// !reply
											confirmClickRecording();
										}); // end confirm3 CB

									}//end if click
									else 
										confirmClickRecording();
								}, "", "", "M");    //end registerMenu
		
		registerMenuCommand( "Toggle Auto-Click-Replay function on all pages "
								+(suspend_replay ? "[disabled]": "[enabled]"),
								async function() { 
									await readPersistentData();
									if (suspend_replay) suspend_replay=false;
									else suspend_replay=true;
									persistData();
	                                suspend_replay=hash_host_list["suspend_replay"];									//window.status="Auto click replay is now "+(suspend_replay ? "suspended.  ": "on.  ");
									salert("Auto click replay is now "+(suspend_replay ? "suspended ": "available ")+ "on all pages.");
								}, "","","u");  


		registerMenuCommand( "Icon for Form Saver pages on/off, toggle "+(reddot?"[on]":"[off]"),
								async function() { 
									await readPersistentData();
									toggleIcon();
									submenuModule.changeName("Icon for Form.*", "Icon for Form Saver pages on/off, toggle "+(reddot?"[on]":"[off]"));
									//submenuModule.positionAt("Icon for Form.*", 1);
									persistData();
									window.status="Form Saver Icon "+(reddot ? "on": "off");
								});    
		/* if( ! FireFox)  PROBlem on Chrome, only localstorage is available so imported data cannot be seen when visiting other websites*/
		// export also.  Storing it as uneval or member in window.name fails when "back" is selected.
		/*   registerMenuCommand("Import Form Data", function(){ */
		/* 	 var users_input=prompt(widener+"\nTo import raw forms' data paste here and click OK.\n\nTo get raw data on Firefox, look in about:config for the name 'hostFormsList', copy its value here." , ""); */
		/* 	 if (users_input != null) setValue(hostFormsList, users_input); */
		/* 	 alert(getValue(hostFormsList)); */
		/* 	 readPersistentData(); */
		/* 	 persistData(); */
		/*     }) */
		registerMenuCommand("Paste Data Into Form", pasteIntoForm, "", "", "P");
		registerMenuCommand("Clear the Forms on the Page", async function() {
			await readPersistentData();
			for(var i=0; i < inputs.length; i++) {
				var input=inputs[i];
				if (input.selectedIndex != undefined) { input.selectedIndex=-1; input.value=-1; }
				if (input.checked !== undefined ) 
					input.checked=false
				input.value="";
			}
		}, "", "", "C");
		registerMenuCommand( "List of pages with saved form information ",
								async function() { 
									await readPersistentData();
									var sizeof,roll="", roll2=[], reply=RegExp( prompt("Give regexp to filter or leave blank for all:"), "i");
									if(reply.source=="null") return;
									for (var webpage in hash_host_list) {
										let fd=true, len=0;
										if (hash_host_list[webpage].form_data) {
											for (var j in hash_host_list[webpage].form_data)
												len++;
											var unevaled=prettyPrintUneval(hash_host_list[webpage].form_data);
											if ( ! reply.test(webpage) && ! reply.test(unevaled)) continue;
											sizeof=unevaled.length;
											roll+="\n=============================================\n"
												+"Webpage address: "+webpage+ "; "+len+" fields, "+sizeof+" bytes:\t\t\n"+unevaled;
											roll2.push(webpage.replace(/^www\./,""));
											//+prettyPrintUneval(unevaled, true);
										} else fd=false;
										if (hash_host_list[webpage].click) {
											if ( ! reply.test(webpage)) continue;
											roll+=( fd ?  " (" : "\n" + webpage  + ", " )  + " click path: "+hash_host_list[webpage].click.xpath.tail(30)+( fd ? ")" : "." ) ;
										}
									} //end for webpage in hash_host_list
									sizeof=stringify(hash_host_list).length;
									//bigAlert(widener+(roll? roll: "none")+"\n \n"+"Size of store: "+Math.round(sizeof/1000)+"k.");
									//var ar=roll.split("\n");
									//ar.sort(function(a,b) { return a.toLowerCase().localeCompare(b.toLowerCase()) });
									//roll=ar.join("\n");
									salert(reply+"\nSee end of list for form values\n"+widener+
										   (roll? roll2.sort().join("\n")+"\n"+roll: " gave no results.")+"\n \n"+"Size of store: "+Math.round(sizeof/1000)+"k.");
								});    



		registerMenuCommand( "Delete saved form information for a site (regexp) ",
								async function() { 
									await readPersistentData();
									var r1=prompt("Give regexp to filter:"), reply=RegExp(r1, "i");
									var sizeof,roll="", roll2=[];
									if(reply.source=="null" || !r1) return;
									for (var webpage in hash_host_list) {
										if (hash_host_list[webpage].form_data || hash_host_list[webpage].click) {
											
											if ( reply.test(webpage) ) {
												if (confirm("Delete form data for webpage: "+webpage+"?")) {
													delete hash_host_list[webpage];
													alert("Deleted store for "+webpage+"\n\n(reload the page to cancel further processing).");
												}
											}
										}
									} //end for webpage in hash_host_list
									persistData();
								});    




		registerMenuCommand( "Export/Import all form data", exportImport);
		//retired      registerMenuCommand("Open Forms' Address/Notebook, alt-a", formsAddressBook, "", "", "A");
		registerMenuCommand("Unhide passwords [once-off]", unhidePasswords, "", "", "A");
		registerMenuCommand("Get/set Script Paste Config Values", getSetConfigs, "", "", "C");
		
		registerMenuCommand("Manually set active input value",function(){
			var ae=submenuModule.activeElement;
			if ( ! /input|textarea/i.test(ae.tagName)) {
				salert("Active element is neither an input not a textarea, please first focus/click mouse on input on page. ");
				return;
			}
			var reply=prompt("Please input to that to which input/textarea should be set:");
			ae.value=reply;
		},"","","I");
		
		// registerMenuCommand("Reload Iframes",()=>{
		// 	$("iframe").each(function(i,el){ 
		// 		el.src=el.src;
		// 	});
		// });
		
		// addEventListener("keydown", function(e) {
		// 	   //console.log("Keydown: charcode "+e.charCode +", keycode"+e.keyCode+", alt "+e.altKey+", ctrl "+e.ctrlKey);
		// 	   if (e.keyCode!=65 || ! e.altKey) return true;// alt-a. alt a
		// 	   if (!e.shiftKey)
		// 	       formsAddressBook();
		// 	   else
		// 	       fillFromAddressBook();
		// }, 0);

	}//registerGeneralMenus();	

	function registerPageMenus() {
		//log("registerPageMenus")
		if(that.pagedone) return;
		that.pagedone=true;
		registerMenuCommand( "                       --------------------------------------------------", function(){});
		
		registerMenuCommand( "Fill-in Form",
								async function() { 
									await readPersistentData("Fill-in Form");
									if (form_data) {
										fillForm();
										inOutSet();
									}
									else
										window.status="No form data to fill";
									//    persistData(); !!
								}, "",0,"O");
		submenuModule.positionAt("Fill-in Form",0);
		
		registerMenuCommand( "Delete stored Form info for this page",
								async function() { 
									await readPersistentData();
									var profile=profiles.current?".  Profile: "+profiles.current : "";
									var request, page_str="\nPage: "+ page_key + profile + (regexp_page_match ? " (regexp match)." : ".") ;
									if (click && form_data)
										sconfirm(page_str+"\n\nTo delete click-replay & form data for this page, choose 'OK'.  "
												 +"\n\nTo delete only the click-replay or to abort, choose 'Cancel'",
												 function(request){
													 if (request)  { page_object={}; wrapup(); }
													 else 
														 sconfirm(page_str+"\n\nOk to delete click recording? ", 
																  function(request) {
																	  if (request) { delete page_object.click; wrapup();}
																	  else return;
																  });
												 });
									else 
										if (click || form_data)
											sconfirm(page_str+"\n\nOk to delete form date for this page?",
													 function(request) { 
														 if (request) {
															 page_object={}
															 wrapup();
														 }
														 else
															 return;
													 }); // end if clk||frm
									function wrapup(){
										inOutSet();
										window.status="Deleted "+(request?"all stored ":"only click-replay")+" info for: "+page_key;
										persistData();
										setLive();
									}
								});//end registerMenuCommand()    
		
		registerMenuCommand( "Toggle Automatic form fill & Click-replay on this page ["+(page_object.automatic?"on "+(page_object.automatic=="on" ? "[implicit]":""):"off")+"]", async function () { 
			await readPersistentData();
			if (page_object.automatic) { //toggle
				page_object.automatic=false;
				persistData();
				window.status="Ending automatic fill-in of forms at page: "+page_key;
				salert("Ending automatic fill-in of forms at page: "+page_key);
			}
			else {
				page_object.automatic=true;
				sconfirm("Page: "+page_key+widener+"\n\nForm data saved by the user shall "
						 +"be filled in for this page automatically from now explicitly on.  "
                         +"Enabling auto replay of clicks on this page will not override the general setting."
						 +"\n\nCancel to explicitly disable auto-fill/click-replay on this page"
						 , function(request) {
							 if ( ! request)
								 page_object.automatic=false;
							 else page_object.automatic=true;
							 window.status="Automatic filling of forms on page "+page_key;
							 salert("Automatic filling of forms is explicitly on at page "+page_key);
							 inOutSet();
							 persistData();
						 } );
			}
		}//end cb function
								, "","","" );
		
		registerMenuCommand("Edit stored Form data and site", function() {
			editFormData();
		}, "", "", "E");

		registerMenuCommand("Append to existing form", function() {
			appendFormData();
		}, "", "", "A");
		
		
		registerMenuCommand("Edit Click Element data", async function() {
			await readPersistentData();
			editClickData();
			persistData();
		}, "", "", "E");
		
		registerMenuCommand( "_____________________________________", function(){});
	} //registerPageMenus()
} //registerMenus()

async function appendFormData() {
	await readPersistentData("Save Form");
	if ( inputs.length==0)
		return;
	useCorrect_page_object();
	saveFormData(null,true);
	if ( ! link )
		addIcon(true);
	setLive();
	inOutSet();
}

function convert_obj_to_array(obj) {
	var ar=[];
	for (var  i in obj) if ( ! isNaN(i) ) ar[i]=obj[i];
	return ar;
}

// function bigAlert(str) {
//    lines=str.split("\n");
//    var entries=lines.length-5;
//    var roll=entries+" entries.  "+(entries>30?"Pages: "+( (entries/30^0) + 1 ) : "" ) + "\n", i=0;
//    while (lines.length) {
//       while ( i < 30) { roll+=(lines[0]?lines[0]+"\n":""); lines.shift(); i++ }
//       alert2(roll, 0.7);
//       roll=widener, i=0;
//    }
// }

function fixMargins() { // see trickle function
	var results = document.evaluate("//*[contains(@style,'margin-left')]", document, null, 6, null);
	var item, len=results.snapshotLength;
	var margin, parent, pos;
	if (len > 0) 
		for(var i=0; item=results.snapshotItem(i), i < len;  i++ ) {
			margin=parseInt(item.style.marginLeft);
			//log("item margin "+margin);
			parent=item.offsetParent;
			pos=item.offsetLeft;
			//log(" item pos "+pos);
			if (pos < 200 && margin > 5) {
				item.style.setProperty("margin-left",(Math.max(0, margin - 35) )+"px","important");
				//log("Fixed margin for "+item+" "+item.id+", "+item.className +", from "+margin+".  At: "+page_key);
			}
		}
	return;
}

function toggleIcon() {
	if (reddot) {
		reddot=false;
		removeIcon();
	}
	else {
		reddot=true;
		addIcon(true);
	}
}

function removeIcon() {
	if (link.parentNode == window.document.body) {
		window.document.body.removeChild(link);
		delete page_object.position;
	}
}

function addIcon(make, override) { //  Called from main witih 'true', called also from toggle & gm cmd.  Append, prepend reddot to body in DOM hierarchy.
	//log("addIcon","link:",link,"reddot",reddot,override,"make:",make);
	if (link && (reddot || override)) {
		var b=window.document.body;
		//log("insertBefore",b,link);
		if (b) b.insertBefore(link, b.firstElementChild);
		//setTimeout(x=>b.appendChild(link),10000); //!!
		img.style.display="";
		//setTimeout(fixMargins, 1000); //!!!!fixMargins
		//   link.style.cssFloat="left";
		//   img.style.cssFloat="left";
	}
	else if ( ! link && make)
		makeReddot();
}

function makeReddot(parent) { // makes a "link" object as: <B><IMG></IMG></B> and registers mouse events on it.
	parent=parent||window.document;
	link=link||parent.createElement("sfsb");
	img = parent.createElement("img");
	img.id="SFSimg"; //img2.id="SFSimg2";
	var lk=link;
	lk.appendChild(img);
	addIcon(); // insertsBefore 1st body el., ie, prepends to body.
	//checkPageLayout();  //!!!checkPageLayout
	setLive(); // sets style.	//   trickleUp ( document.elementFromPoint(pos.x, pos.y + 50)) } catch(e){};
	lk.id="hostFormsListButton";
	lk.className="SFSButton";
	var dragged=0;
	setTimeout(function(){
		$(img).draggable({
			stop: function( event, ui ) { event.dragEv=true;
										  event.ui=ui;
										  dragged=0;
										  mouseUpCB(event);},
			drag: function( event, ui ) { log("Dragg",dragged);dragged++;}
		});//.draggable()
	},100);	// $(lk).draggable();	//$(reddot).draggable();
	lk.addEventListener("mousedown",function(e){e.preventDefault(); e.stopPropagation();});
	lk.addEventListener("click",function(e){e.preventDefault(); e.stopPropagation();});
	lk.addEventListener("contextmenu", async function(e) {  // right click
		e.preventDefault();   
		e.stopPropagation();
		await readPersistentData("contextmenu click");
		inOutSet();
		window.document.body.setAttribute("onClick", "return false;"); //!!!
		useCorrect_page_object();
		sconfirm("Simple Form Saver, saving data at: \n\n\t"+page_key+(profiles.current?"  User profile: "+profiles.current+"":"")+".  Ok/Cancel?",function(reply){
			if (!reply) return;
			saveFormData();
			setLive();
		});
	}, true);
	lk.addEventListener("dblclick", function(e) {
		e.preventDefault(); e.stopPropagation();
		console.log("dbl Click on sfs reddot.");
	});
	lk.addEventListener("mouseup", function(e) { // click // was "mousedown"
		mouseUpCB(e);}, true);
	
	async function mouseUpCB(e) {
		//console.log("mouseUpCB",e.dragEv,dragged); // This event passes on to th draggable to stop drag.
		if (dragged) return;
		if(e.dragEv) {
			await readPersistentData("drag");
			page_object.position=e.ui.position;
			persistData(); 
			console.log("Mouse was dragged, saved pos:", e.ui.position);
			return;
		}
		dragged=0;
		if (e.button==0 ) {        //left button 
			await readPersistentData("click");
			inOutSet();
			if (form_data)
				fillForm();
			else {
				useCorrect_page_object();
				if (click) replayClick(true);
				saveFormData();
				setLive();
				persistData(); 
			}
			//persistData(); //!!
			log("done but 0");
		}
		if (e.button==1) { // middle button was clicked
			e.preventDefault();   
			e.stopPropagation();
			log("M-up END propa.");
			e.stopImmediatePropagation();
			await readPersistentData("mid click");
			var size, promptpop=await window.openFormStatus(); //submenuModule.revertIframeSize );
//			size=[promptpop.width(),promptpop.height()],
//			submenuModule.resizeIframe(size);
		}
		if (element_to_highlight) element_to_highlight.style.borderStyle="none"; 
	} //end mouseUpCB()
}//makeReddot()

async function setLive() {
	if ( ! link)
		return;
	var redSquareRing="data:image/gif;base64,R0lGODlhCQAMAIABAP8zM////yH5BAEKAAEALAAAAAAJAAwAAAIPhI+pwdvJgpPwoTiz2rsAADs=";
	redSquareRing="data:image/gif;base64,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";
	var	redDot_img="data:image/gif;base64,R0lGODlhrAC1AOMJAOIAAOIABeQAAOQABeYAAOYABeIFAOQFAOYFAP///////////////////////////yH5BAEKAA8ALAAAAACsALUAAAT+8MlJq704660JeUThjeRRSB+nrmzrvnA8CSNN2Dape3gq/8CgUHY75HI8pI52WHoKNAHiMKxar66TISmIdkNR0Y50JAxuBES6BsK639Wa3KwW55qAMZopUo/8IyZGJwJwhocdIB5GfyE6TQSQSktKXzkDkDcCUIGInocGDzQFmUg2A1FNgHpPZHMFajliCJwFfZ+4Q3U0BlE7qb6brqxow181Uk++kR8+uc8uaGJixNXW19gj0NscTjzZ4OHgAlvc5hVJrcvi7O3e5+aaBFtPTZPu+OyjzvCHPXXUpOUbGA6SEUD94IyAFYvMFxMEI2K7JC+hmyMAOO3IJLGjni7+Irpksmil2K9NX2yt8sjyFUQcJIOAPNOyJkEaCGjEJMKMo82f4qQYCMlv5wZHke4BXWptnySjKuzYWse06rVRUE5AxSDrjFKrYF35tLcVXQ9mA1aGXftNE000ZVGMasJJgE+2a1FR8oAg7gNI1I7hHfyLh51CRu3yJcz42jS4O/dQbUx5jsmi3ApX3uxqXxGLN9RggvKV82A+IwbAQyLGSGnTYYkuhLzNCYBTsCuT0oQx843dizy8zU3ZJyYb0KTZE6GROGVaIYUtw6V4jfPrY2ik9fBJ8mXs17sIRuQNvHkpRD0lGy3YPHFfUJpQgfM46Wv3jZeR8ofmtnD84DH+l8MbHEEyHICmoYRALHi48Y0wzSEImwB6mSQCFpEEJKGENmglhCPAbbihXogJcZaIKP5WogwK3pdibqSE9KE9L6I44A+XuVgjZ8mYgVkLrewoYko/RoWajkLyqFMMSTZ5wwt3OZniCxpKiaKHKzBi5Y4uHLjlkEVe8N2XHKaxwmJkptiFCsKkueN8R43pJn5Lxjknlxu0dyeA4q14gQG0VLenhFiK6eWg5zFTpHhyIgremhjwhaSjPB6g2gU0RUhpeHtwV8E6k25KmBdc5SSqiGJadmp4UfRlgUlarprbMSq9ipSsnJLxaSTL4ZorJBToYIuvxBHFxARulUGsaRz+XYjCskNKECq0m0lLLYcSVHktdhNEua1zyH5Lpyji0tlouTAqgu667F7VrnmmvkvcAxXKm+C09lolUr4T3sovZ97+O5imAjembcF4HYwwWwEvHJa6DhMWb8SnUcwYxBY/fGjGS+mEL8f5PAsyWCKPrK/CJrdES8pVkcsyU9a+LBF0mjiLscz5+FQizgTtxqinN/MsjmytlCw0Pqg5czTSAj0Z7tL6WBYs1Ow8Vme4Qw1LdTUivAQFOsGptbUOgABbASknjq3HLJtc0KraxCCR06ukwV0NKtSI+THPYtDj57OC2v2IPEWCca7gfwdrh+BLiFBOBooy/lGYqkqOQNb+iU+9t8xNdNOW5EidefjWUYgOOgmFdnC6HHDG+bYZesrshXgtRBk7y16BGKbbD/63dBi7X9BcvXynE3ykG8teRuYcMPGN2CYzIgYQPn7+8uLMszCZ7EtQ/zPPWl4tg+OjgzzKEI0I/UUVzKEMss3o/965FV1APzJyVrjPcekYqp899W2xX7548Y1D/EeA8qIRCRAhHx4g0F74U880HrguJLTOEH/YXr4OMBSU5IIZt4MgM6AxAP29y1QXxEXEzheP+GxkW5OQwv88kaMQXUsNNIJUQlrzG3EpIXWrecvm0nQE/pGkFQ2TVTDUABXfiasAW5BCWcqnRFP5hV6RuxbrEKFSi2P0wlEU+mIIvnZFCuDkC8kj4lTE46oyziBtg6KQOtyYKg3eaSgEAAAd3SYVeoBxhm70QB7iWMA9HmWIhAKkIZ92JykuUns+yqHuzrOy4ADreI+0ACDiZYnwQFESrZheJn9wjFT4KzexKoIiR3kUUEoiiZTxQghSyMrxDaJ6zvnZKmsZjaxwSgqo4CUGqUgYEOxSmDyZFzKhcaJOFgNUBBxDFJGQOySIYpnwsNB/KtSjEDTkNxrBSUhy4prpYDMhKZAhD3S2DKXgoJQyOmdcxhQY1NlzDseUJ0mQg5wRBIA3NPgn0HgZAQA7";
	var redDot_off="data:image/gif;base64,R0lGODlhCAAIAMIEAO9mZvBmZu9nZu9oZwAAAAAAAAAAAAAAACH5BAEKAAQALAAAAAAIAAgAAAMPSDTRSi0GKKu9eNoHmlAJADs=";
	link.addEventListener("mouseover",function(){ 
		if ( ! (form_data||click)) img.src=redDot_img;  
	}, false);
	link.addEventListener("mouseout",function() {
		this.style.opacity=1; 
		if ( ! (form_data||click) ) img.src=redDot_off;
	}, false);
	link.style.opacity="1";
	link.style.rightMargin="10px";
	var ist=img.style;

		ist.cursor="pointer";
		ist.position="fixed";
		ist.top=!iframe ? "5px" : 0;
		ist.left=!iframe ? "18px" : 0;
		if (page_object && page_object.position) {
			ist.top=page_object.position.top+"px";
			ist.left=page_object.position.left+"px";
			var msg="Simple Form Saver, user set image position to left:"+ist.left+", top:"+ist.top;
			console.info(msg);
			window.status=msg;
		}
		ist.setProperty("display", "inline", "important");
		ist.setProperty("z-index", "2147483644", "important");
		ist.setProperty("min-width", "6px", "important");
		ist.setProperty("font-size", "small", "important");
		ist.setProperty("box-sizing", "content-box", "important");       
		//zIndex=9999;
	
	if (form_data || click) {
		registerMenus(true);
		img.src=redSquareRing;
			ist.setProperty("border","", "important");
			ist.setProperty("border-style","outset", "important");
			ist.borderWidth=".23em";
			ist.opacity=1.0;
			ist.borderColor="rgba(200, 200, 200, .9)";
			ist.height="14.4px"; //".6em"
			ist.width="12px"; //".5em"
			ist.height = ! form_data ? "10px" : "12px"; //"1.0em" : "1.5em";
			ist.width=ist.height;  //!form_data ? "1.0em" : "1.5em";
			var newline=(plat_chrome?"&#10;":"\n");
			link.title="Click to Fill in Form (&/or replay a recorded click). "
				+newline+"Right click to Save form.  Drag n Drop to move icon."
				+newline+"Middle click "+(iframe?"to open iframe in own window/tab in order to allow access to GM settings for this part of the page."
										  :"to get forms' status info.")
				+ (iframe ? "  (Within iframe: "+location.href+")." : "" ) + userProfileLine
				+ (otherProfiles.length ? newline+"Other profiles have data stored for this page (->icon pulsation)."
				   : "" ) +newline+addedByLine;
			link.setAttribute("noautohide","true");
		
		full_icon=true;
	} 
	else { // no form data, nor click
		if (click)       registerMenus(true);
		img.src= redDot_off; 
		img.height=img.width="10px";
		ist.height="";
		ist.width="";
		ist.height=".5em";
		ist.width=".5em";
		if (!click) {
			ist.borderStyle="none";
			ist.borderWidth="";
			ist.setProperty("border","none", "important");
			$(link).prop("title","Click once to save form data "+(click?"or to replay a click":"")+"; middle-click to get forms status info."
						 + (iframe ? "  (Within iframe)." : "" ) +"  Can drag and drop to move icon.  See userscript menu to remove.  At: "
						 +page_key
						 +userProfileLine
						 +(otherProfiles.length ? "\nOther profiles have data stored for this page (->icon pulsation)."
						   : "") +addedByLine);
		}
		
		//img2.style.display="none"; 
		full_icon=false;
	} // end else
	if (reddot==false)
		img.style.display="none";
	else
		img.style.display="";
	
	var iel=$(img), bwidth=form_data||click ? 9 : 5;
	if (profiles.current=="") bwidth=0;
	iel.css({border:"dotted "+bwidth+"px rgba(0,0,0,0.7)"});
	//log("Img Bdr width",bwidth,"profiles.current",profiles.current,"others:",otherProfiles.length,otherProfiles);
	if (otherProfiles.length) {
		pulsate(iel,3);
	}
}// end setLive()

function pulsate(el,times,recur) {
	var that=pulsate;
	if (!recur) that.orig={ w:el.width(), h:el.height() };
	var  factor=1.5, w=that.orig.w, h=that.orig.h, wf=w*factor, hf=h*factor;
	el.animate({width: wf, height: hf, opacity: 1}, 700, function() {
		el.animate({ width: w, height: h, opacity: (times>1?.5:1) }, 700, function() {
			if (times==1) return; 
			pulsate(el,--times,true);
		});
	}); 
};

function inOutSet(set) {
	if ( full_icon 
		 || ( ! form_data && click ) 
		 || set
	   ) {
		if ( ! img.style.borderWidth)
			img.style.borderWidth=".1px";
		if (!this.prev) {
			img.style.setProperty("border-style", "inset", "important");
			this.prev=true;
		}
		else {
			img.style.setProperty("border-style", "outset", "important");
			this.prev=false;
		}
	}
}

function checkPageLayout(){
	var logo, p, gp; logos=getByIdOrClass("logo-img-2", "logo", "gh-log", "p-logo", "mw-panel", "logocont");
	for( var i =0; logo=logos[i],  i < logos.length; i++ ) {
		logo.style.setProperty("margin-left", "0px", "important");
		logo.style.setProperty("z-index", "0", "important");
		if ( ! checkPageLayout.count) { checkPageLayout.count=10;}
		logo.addEventListener("DOMAttrModified", checkMargins, 0);
	}
	return;
}

function checkMargins(e){
	log("checkMargins");
	if (e.target != this) return;
	if ( this.style.marginLeft[0] != 0) this.style.setProperty("margin-left", "0px", "important");
	if ( ! -- checkPageLayout.count ) 
		for( var j =0; logoj=logos[j], j < logos.length; j++ ) 
			logoj.removeEventListener("DOMAttrModified", checkMargins, 0);
}

function findRenamedInput(name, i, saved_value) 
{
	var n, result, saved_data=page_object.form_data;
	for (n in saved_data) {
		if ( i == saved_data[n].i ) {
			result=saved_data[n].v
			break;
		}
	}
	return [result, n, i ];
}

function getElementsByTagNames(list,obj, full) {
	if (!obj || ! obj.getElementsByTagName) var obj = window.document;
	var tagNames = list.split(',');
	var resultArray = new Array();
	if (obj.tagName && list.match(obj.tagName.toLowerCase())){
		resultArray.push(obj);
	}
	for (var i=0;i<tagNames.length;i++) {
		var tags = obj.getElementsByTagName(tagNames[i]);
		for (var j=0;j<tags.length;j++) {
			if (  tags[j].type   
				  &&   !   /^(submit|hidden|image|reset|button)$/.test( tags[j].type.toLowerCase() )
				  &&   !   tags[j].hasAttribute("hidden")
				  &&   ! invisible(tags[j])
				  &&   ! (tags[j].disabled == true) || full  ) 
			{
				resultArray.push(tags[j]);
			}
		}
	}
	var testNode = resultArray[0];
	if (!testNode) return [];
	if (testNode.sourceIndex) {
		resultArray.sort(function (a,b) {
			return a.sourceIndex - b.sourceIndex;
		});
	}
	else if (testNode.compareDocumentPosition) {
		resultArray.sort(function (a,b) {
			return 3 - (a.compareDocumentPosition(b) & 6);
		});
	}
	return resultArray;
}

function invisible(el) {
	var jel=$(el);            	//var style=window.document.defaultView.getComputedStyle(el, null)||{};
	//var jcss=jel.css(["visibility","opacity"]);
	return !jel.is(":visible"); // || jcss.visibility=="hidden" || jcss.opacity==0;

	// note, opacity 0 may still appear in outline// jq :visible is true if it takes up space on the page.
}

function chopLongString(form_string) {
	if (form_string.length > 1700)   form_string=form_string.replace(/[\t\n]/g," ").replace(/\u26ab/g,tab_bullet).replace(/\t/g,"")
	return form_string;
}

function getStatusInfo() {try{
	var data= form_data, n, list="";
	for (n in data)
		if (data[n].p)
			list+=n+",\t";
	if (list) list=list.substring(0, list.length-2);
	var page_list=null, page_list2="";
	//   if ( ! form_data) { //////////////////////////////////////
	page_list=bullet_tab;
	//	var tinputs = getElementsByTagNames('input,textarea,select', null, true);
	var tinputs = getElementsByTagNames('input,textarea,select');//was showing submit buttons with no text.
	for(var i=0; i<tinputs.length; i++) {
		var inp=tinputs[i];
		var type=0, option=0;
		if (inp.tagName =="INPUT")
			type=inp.type
		else {
			type=inp.tagName
			option="";
			if (inp.tagName=="SELECT" && Number(inp.value)) {
				var options=inp.options;
				option="\t\t(~"+options.item(inp.selectedIndex).textContent.replace(/[\n\t(  )]/g,"")+")\t";
			}
		}
		var val=inp.value;
		var style=window.document.defaultView.getComputedStyle(inp, null)||{};
		var visibility=( (style.display=="none" || style.visibility=="hidden") ? " (invisible)" : "");

		page_list+=(inp.name?inp.name:"null")+"::"
			+(val? ( inp.type=="password" && ! unhide ? val[0] + "..." + val[val.length-1]+"["+val.length+"]" 
					 : val  +(unhide?" (unhidden":"")      )
			  :"null")
			+(option?option:"\t\t\t\t")+"\t"+type+visibility+"\n"+bullet_tab;
		if (inp.type=="password" && ! unhide)
			page_list2+=val[0]+"..."+val[val.length-1]+"["+val.length+"]";
		else
			page_list2 += val +"\n";
	} // end for inputs[].
	page_list=page_list.substring(0,page_list.length-2);
	// } // end if ! form_data ///////////////////////////////////////
	if ( form_data) { page_list=null }
	var ip_names="";
	for (var i in input_names) ip_names+=i+", "; ip_names=ip_names.substr(0, ip_names.length-2);
	var form_string=page_list || formDataToFromString(true);
	form_string=chopLongString(form_string);
	log("get xpath ");
	var click_elem=getXPathElem();
	var user_profile_info=(profiles.current?"\nUser profile: "+profiles.current+" (non default -> icon decor).":"")
		+(otherProfiles.length? "\nOther page relevant user profiles: "+otherProfiles:"");
	var forms_info =
		( form_data ? "Previously saved values and names are listed below."+user_profile_info
		  : "Page Values and form names have not been saved for this page")
		+"\nFormat: name ["+mdash_char+"default]::value"+( form_data ? "":"\t\tType of (name::value)")+"."
		+ (form_data  ? "\n\n"+bullet_tab+form_string : "\n\n"+form_string+"" )
		+  (list ? "\n\nThere are duplicates whose values may differ from the above for the input(s) named:\t "+list+"."  : "")
	var settings_info=""
		+"Names of all inputs: "+ip_names
		+"\n\nAutomatic form filling on this page "
		+ (page_object.automatic ? "is on.  " :  (page_object.automatic !== false ? "has not been turned on, and is off.  " : "and click-replay, have been explicitly disabled.  Check the general setting of auto replay  below."))
		+(click ? "\nClick auto-"+(click.ctrlShift?"delete":"replay")+", is set for"
		  +(click.own_href == "any" ? " any page with this path.  " :  " only this specific page.  ")
		  +"Page's element (XPath) to be auto-"+(click.ctrlShift ? "deleted":"clicked")+" is:\n\n\t\t"+ ( ! (click_elem && click_elem.xx) ? page_object.click.xpath  : page_object.click.xxpath + " as xpath" ) +""
		  : "Nothing to replay on this page.  " )
		+ ( click && ! click_elem
			? " (the element is not currently on this page" + (click.href ? " but has href, " + click.href : ""  )  +  ")." 
			: ( click && (href == click.own_href || click.own_href == "any" )
				? (click?"\n\nThe element is present on the page.\nIts value and text is: "+click_elem.value+" "+click_elem.textContent+", it's href is:"+click.own_href : "none")
				: (click?", the element is on page but it is for another webpage: \n\t\t"+click.own_href : "none" )))
		+(click && click.onclick_js? "\n\t\tJava to replay: "+ click.onclick_js    : "")
		+(click && click.further_clicks? "\n\nFurther clicks are on elem(s) (XPath(s)): "+click.further_clicks.join(", "):"")
		+ "\n\n"+ (suspend_replay ? "Click auto-replay is suspended in general, ie, suspended for any webpage." : "Click auto-replay on all pages is not suspended.")
		+ (inputs.length ? "\n\nNumber of useable input areas on page (some may currently be hidden): "+(inputs_on_page) : "")
		+"\nIcon status is: "+(reddot ? "show.": "don't show.")
		+(iframe ? "\nHTML Frame." :"")
		+"\nCurrent values:\n"+page_list2;
	var status="Page "
		+ ( (form_data || click) ?  "stored: " : "not stored: ")
		+ page_key + (regexp_page_match ? "  (matches as regexp within, "+regexp_page_match+", specifically)." : ".")
		+ user_profile_info
		+ "\n<a class=submenu_c href='#' style='float:right;position:relative;top:0;'>Open 'Simple Form Saver' submenu</a>"
		+ widener;
	if (form_data)
		status +="\n\n"+forms_info+"\n\n"+settings_info;
	else { 
		if (page_list)
			status+="\n"+settings_info+"\n\n"+forms_info;
		else
			status+="\n"+settings_info;
	}
	if (click && click_elem) {
		element_to_highlight=click_elem;
		let ehs=element_to_highlight.style;
		ehs.borderColor= "red"; ehs.borderWidth= "10px" ; ehs.borderStyle= "solid" ; 
	}
	
	var flen=window.frames.length;
	if (flen) status+="\n\n"+"Window contains "+flen+" iframe(s), please use icon to fill or save forms within iframes or invoke GM menu shortcut (alt-m) whilst focus is in iframe.";

	return status;
	}catch(e){console.log("Get status error,",e);}
}//getStatusInfo()


function getXPath(elt, counting) {
	function getElementIdx(elt) {
		var count = 0; // zero meaning only tag of that type here.
		for (var sib = elt.previousSibling; sib ; sib = sib.previousSibling)	{
			if(sib.nodeType == 1 && sib.tagName == elt.tagName)	{
				if (count==0) count=1;
				count++;
			}
		}
		if (count==0)
			for (var sib = elt.nextSibling; sib ; sib = sib.nextSibling)	{
				if(sib.nodeType == 1 && sib.tagName == elt.tagName)	{
					count=1;break; //1 signalling 1 of many
				}
			}
		return count;
	}
	var path = "";
	for (; elt && elt.nodeType == 1; elt = elt.parentNode)    {
		idx = getElementIdx(elt);
		xname = elt.tagName.toLowerCase();
		if (idx > 0) {
			if (elt.id && ! counting && ! /[0-9]{3,}/.test(elt.id) )
				xname+="[@id='"+elt.id+"']";
			else 
				xname += "[" + idx + "]";
		}
		path = "/" + xname + path;
	}
	return path;	
}

function getXPathElem() {
	if ( ! click) return;
	var snap=uwin.document.evaluate(click.xpath,uwin.document,null,6,null), xx, xpresult;
	log("Called evaluate on "+click.xpath+", results: "+snap.snapshotLength+" page key: "+page_key);
	var elem=snap.snapshotItem(0);
	if (snap.snapshotLength > 1 || snap.snapshotLength == 0) {
		snap=uwin.document.evaluate(click.xxpath,uwin.document,null,6,null);
		xx=true;
	}
	if (snap.snapshotLength > 0) 
		xpresult=snap.snapshotItem(0);
	if (!xpresult) log("No xpath result, try id and class if singular, id: "+click.id+", class: "+click.className+".");
	if (!xpresult && click.id) { xpresult=document.getElementById(click.id); if (xpresult) xpresult.classed=click.id;}
	if (!xpresult && click.className) {
		xpresult=document.getElementsByClassName(click.className);
		if (xpresult.length==1) { xpresult=xpresult[0]; xpresult.classed=click.className;} else xpresult=undefined;
	}
	if (xpresult && xx) xpresult.xx=true;
	log(" got xp: "+xpresult);
	return xpresult;
}

function useCorrect_page_object() {
	if ( ! regexp_page_match ) return;
	log("cfm");
	var reply=confirm( "This page's form data is already stored under "
					   + page_key + " (as regexp).  It matches the current page, "+regexp_page_match+"." + widener
					   +"\n\nActual page matched a more general page on which form data was previously stored."
					   +"\n\nClick 'OK' to save under general page that is already stored."
					   +"\n\nClick 'Cancel' to save only for this specific page."  );
	if ( reply ) return;
	reply=site+window.document.location.pathname;
	var po=hash_host_list[reply];
	if ( ! po) po=new Object(); 
	hash_host_list[reply]=po;
	//po.click=click; // copy old data?
	persistData();
}

function persistData() {
	hash_host_list["suspend_replay"]=suspend_replay;
	hash_host_list["reddot"]=reddot;
	//log("Saving data --- persistData, page_key",page_key,"profile:",profiles.current,"hostFormsList",hostFormsList,"page_object",page_object,countMembers(page_object),"reddot:",reddot);
	if (page_object && countMembers(page_object))
		hash_host_list[page_key]=page_object;
	else 
		delete hash_host_list[page_key];
	updateVars();
	//console.log("uneval:",hash_host_list,typeof hash_host_list,"unevaled:",uneval(hash_host_list));
	//var stringed=stringify(hash_host_list);
	//console.log("persistData, write to hostFormsList:",hostFormsList,"to val:", {val:stringed,hash_host_list:hash_host_list});
	setValue(hostFormsList, hash_host_list);
	write_once=true;
}

async function readPersistentData(origin) {
	//    if ( write_once == false) return;
	hash_host_list=await getValue(hostFormsList,{}); 
	//log("readPersistentData, origin: "+origin+", hostFormsListname:",hostFormsList,"got:",typeof hash_host_list," val:",hash_host_list);
	if (!hash_host_list)  hash_host_list=new Object();
	if (countMembers(hash_host_list) == 0)   {
		hash_host_list["suspend_replay"]=suspend_replay;
		hash_host_list["reddot"]=reddot;
	}
	updateVars();
	write_once=false;
	//log("readPersistentData key:",page_key,", got page_object",page_object,"break?",page_object.breakIframes);
	//var jsonlen=JSON.stringify(hash_host_list).length	//log("Read Persist  key: "+  page_key + "\n\nPage data:\n "+uneval(page_object.form_data), "Data store size:",jsonlen,"Bytes")
}

function updateVars() { //called after reading and before writing underlying data.
	page_key=getPageKey();
	checkForInputs();
	getMatchingPageObject();   // sets globals page_key,page_object and regexp_page_match.
	form_data=page_object.form_data;
	click=page_object.click;
	suspend_replay=hash_host_list["suspend_replay"];
	reddot=hash_host_list["reddot"];
	//log("Updated vars, form_data is: ",form_data,", click:"+click+", key: "+page_key+", reddot:"+reddot);
}

function getMatchingPageObject(key_in) { //dual function updates global vars or returns a page object for a given key;
	var key=key_in||page_key;
	var po=hash_host_list[key];
	var regexp_match=false;
	if ( ! po) {
		for(var i in hash_host_list) { // i as regexp in longer strings site,page_key
			if (i.match("/")) continue;// only bare site name matches all.
			if (site.match(i) || key.match(i) ) {
				//log("Match found in hash for:"+i+", in "+site+", &/or in page key: "+key)
				po=hash_host_list[i];
				regexp_match=key;
				key = i;
				break;
			}
		}
		if ( ! po)
			po=new Object();
	} // end if ! po
	if (!key_in) { //update globals
		page_key=key;
		page_object=po;
		regexp_page_match=regexp_match;
	}
	else return po;
}//end matchPageObject()

function getPageKey(url) {
	site=getSite(url);
	if(!url) url=window.document.location;
	var key=site;
	if (key) {
		try { // pathname gives file after site name but before any additional stuff eg, after a '?' in long href.
			key+=url.pathname; } catch(e) { key+=window.document.title.substring(0,10);}
	}
	else
		key=window.document.title.substring(0,40);
	return key; 
}

function getSite(url) {
	var  host;
	var domain_regexp=/((\.\w+\.|^)\w+.\w*$)/;
	if (!url) url=window.document.location;
	try{ 
		host=url.host;
		href=url.href;
		if ( ! host)
			host="localfile";
	}
	catch(e){ 
		host="";
		if (window.document.title)
			console.log("can't get site for doc: "+uwin.document.title)
	}
	try { if ( ! href ) href = window.document.title } catch(e)  { console.log("Cant get doc href or title")}
	return host;
}

function insertCode(code, win) {
	if (code.length > 1) {
		if (!win) win=window;
		var script = win.document.createElement("script");
		script.type = "application/javascript";
		script.textContent = "(function() {" + code + "})();"; // for to exec anonymously, ie "(funcX)()" a function.
		win.document.body.appendChild(script);
		return true;
	}
}

function replayClick(manual) {
	setTimeout(function() { replayClickSwapped(manual); }, 1000); /// 20/6 tc, was 3650
}

function replayFurtherClicks() {
	if (!click.further_clicks) return;
	for(var i=0, delay=0; i< click.further_clicks.length; i++, delay+=200) {
		setTimeout(function(j) { 
			var elem, snap=uwin.document.evaluate(click.further_clicks[j],uwin.document,null,6,null);
			console.log("Replaying further clicks after 2 sec pause, on xpath, "+click.further_clicks[j]+", # matching elements: "+snap.snapshotLength);
			elem=snap.snapshotItem(0);
			if (!elem) return;
			elem.relatedTarget=elem;
			fakeClick(elem);
			var tstamp=(new Date().getTime());
		}, 2000+delay, i);
	}
}

//function replayClick(manual) {
function replayClickSwapped(manual) {
	//console.log("replayClickSwapped "+click+", "+suspend_replay);
	if ( ! click || (  ( suspend_replay  || page_object.automatic === false )
					   &&  ! ( manual || page_object.automatic === true)   )) {
		if (suspend_replay && click)
			console.log("Click auto-replay suspended from playing.  ");
		return;
	}
	//console.log("ReplayClick(), click's href: "+click.own_href+".  href: "+href+", manual "+manual);
	try{
		var prejudice={};
		function execInPageContext(nohref) {
			var code="";
			if ( ! nohref && click.href) 
				code+=click.href.substring(11);
			code+=";"+(click.onclick_js ? click.onclick_js : "");
			return insertCode(code);
		}
		if ( checkIfIntervalLongEnough(prejudice) || manual) {
			if (click.xpath && ( href == click.own_href || click.own_href == "any"  ) ) {
				elem=getXPathElem();
				log("replay-- matches href or href=any, elem.innerHTML: "+(elem?elem.innerHTML:""));
				var java_run;
				if ( ! elem ) { 
					if ( regexp_page_match) return;
					no_element_for_click=true;
					//console.log("replay-- doc, "+page_key+" changed.  Recorded Xpath stale: "+click.xpath    +(click.href ? ".  Try href: " + click.href  : "" )  +  (click.onclick_js ? ".  Try javascript: "+click.onclick_js : "" ) ) 
					if (click.href && click.href.substring(0,11) != "javascript:" ) {
						java_run=execInPageContext(true);
						window.document.location = click.href; 
					}
					else
						java_run=execInPageContext();
					if (java_run || click.href)
						persistClickEvidence();
				}
				else { // else ! !elem
					if ( click.ctrlShift) try { 
						elem.parentNode.removeChild(elem); var msg="Auto-Deleted element "+click.xpath; window.status=msg;  } 
					catch(e) { var msg="Can't delete "+click.xpath; window.status=msg; console.log(msg); }
					else {
						//console.log("replayClickSwapped have click:",click,"elem",elem);
						no_element_for_click=false;
						log("replay-- Have element,  path: "+click.xpath +  (click.href ? ".  Try href: " + click.href  : "" )   +(elem.href ? ".  Try href: " + elem.href  : "" )  +(click.onclick_js ? ".  Try javascript: "+click.onclick_js : "" ) 
							+ ", elem.click:"+ (elem.click?" true.":" none.")); 
						if (elem.tagName=="A" && elem.href != "" && elem.href.substring(0,11) != "javascript:" ) {
							if (elem.target == "_blank")  	unsafeWindow.open(elem.href, elem.target); 
							else window.document.location = elem.href; 
						}
						else {
							if (click.href && click.href.substring(0,11) == "javascript:") 
								java_run=execInPageContext()
							else if (click.onclick_js) 
								java_run=execInPageContext()
						}
						//console.log("if elem.click",elem.click," then call elem.click()");
						if (elem.click ) {
							//tc,6/20 c/o 1l.
							//triggerEventSeq($(elem),null,"click");
							// var scripts=window.document.getElementsByTagName("script");
							// for (var i in scripts)    if ( scripts[i] && ! /(^\s*$)/.test(scripts[i].textContent || " ") )  try { 
							//     eval.call(uwin, scripts[i].textContent);  } catch(e){ console.log("\nParse Error: "+e); } 
							///above needs remove scripts too.
							//eval ("elem.click()"); // an input
							//dblcheck triggering of event: 
							//elem.addEventListener("click", function() {alert("clicked"); }, 0);
							// If elem type is "submit" within a form trigger "submit" event on form element.
							let formel=$(elem).closest("form");
							if(formel.length) {
								//console.log("call formel.submit",formel,formel.submit);
								formel[0].submit();
							}
							else elem.click(); // tc, 6/20 u/c 1l.
						} 
						else {
							// tc, 6/20, +1l, c/o 3l.
							triggerEventSeq($(elem),null,"click");
							// var pseudo_event = window.document.createEvent("MouseEvents");
							// // type, canBubble, cancelable, view, detail,           screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget)
							// pseudo_event.initMouseEvent("click", true, true, window, 1, 0,   0,              0,          0,      false,   false, false, false, 0, null);
							// elem.dispatchEvent(pseudo_event);
						}
					} // else ! click.ctrlShift
					persistClickEvidence();
				} // end if/else have Xpath elem.
				var msg="Info: Finished replaying click/delete, at: "+site
					+ ( elem ?  ".  Xpath ok "
						+ (!elem.xx ? click.xpath : click.xxpath+" as xpath")+".  " 
						+ (elem.classed ? elem.classed+" class used, Xpath ignored. ":"")
						+ ( click.ctrlShift ?  "Success, removed element.  " 
							:  ( elem.tagName=="A" ? "Visited link "  +  (elem.target == "_blank" ? " in new window.  ": ".  " ) : "")
							+ (elem.click ? "Invoked input tagged: "+elem.tagName+"; text: "+elem.textContent+"; class, "+elem.className+"; value: "+elem.value+", with click() function.  " :  "Created a pseudo click event.  ")   )
						:     ".  Failed, Xpath not on page.  " 
						+ (click.href ? "Visiting link "+click.href + ".  " :  "") )
					+ (java_run ? "Ran javascript code: "+click.onclick_js : "" );
				hash_host_list.msg=msg;
				//persistData();
			} // end if click.xpath
		} // end if checkIfIntervalLongEnough
		else { // last click replay was < 10 secs ago.
			if ( ! prejudice.obj) { // prevent reload and auto replay on reload looping here.
				persistClickEvidence(10000);// to prevent looping here, from reload(false) below.
				console.log("Simple Form Saver--reload page "+page_key);
				uwin.document.location.reload(false); 
			}
			return true;
		}//end else
		replayFurtherClicks();
	} catch(e) {console.log("Replay error, "+e.lineNumber+" "+e); throw(e); }
}

function persistClickEvidence(prejudice) { //if click causes reload ensures no looping.
	var click_evidence={};
	click_evidence.tstamp=(new Date().getTime() + ( prejudice ? prejudice : 0));
	if (prejudice)
		click_evidence.prejudice=true;
	page_object.click_evidence=click_evidence;
	sessionStorage.click_evidence=stringify(click_evidence);
	//persistData();
	//console.log("persistClickEvidence ",click_evidence.tstamp,", prej: ",prejudice, "ls.ce",sessionStorage.click_evidence);
}

function checkIfIntervalLongEnough(prejudice) {
	var result=true, stamp=new Date().getTime(), dc;
	try { dc=parse(sessionStorage.click_evidence); }catch(e) { sessionStorage.removeItem("click_evidence"); }; // prev version may have left bad string.
	sessionStorage.removeItem("click_evidence");
	page_object.click_evidence=dc;
	var dc=page_object.click_evidence;
	if (dc && dc.sealed) {
		var diff=stamp - dc.tstamp;
		//console.log("Interval since last auto-click: ",diff/1000,".  If < 5 no auto re-click.");
		if ( diff < 5000) 
			result=false;
		if (diff < 0 && prejudice)
			prejudice.obj=true;
		if (prejudice) delete page_object.click_evidence;
		//persistData();
		sessionStorage.click_evidence=stringify(page_object.click_evidence);
	}
	//console.log("checkIfIntervalLongEnough: "+result+" "+(dc? "s: "+dc.sealed+" t: "+dc.tstamp+" p:"+dc.prejudice:""));
	return result;
}

function fakeClick(target) {
	var e = window.document.createEvent("MouseEvents");// create event
	var pseudo_event_md = window.document.createEvent("MouseEvents");// create event
	var pseudo_event_mu = window.document.createEvent("MouseEvents");// create event
	var pseudo_event_click = window.document.createEvent("MouseEvents");// create event
	//type, canBubble, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget
	pseudo_event_md.initMouseEvent("mousedown", true, e.cancelable, window, e.detail, 
								   e.screenX, e.screenY, e.clientX, e.clientY, 
								   e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 
								   0, target.relatedTarget);
	pseudo_event_mu.initMouseEvent("mouseup", true, e.cancelable, window, e.detail, 
								   e.screenX, e.screenY, e.clientX, e.clientY, 
								   e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 
								   0, target.relatedTarget);
	
	pseudo_event_click.initMouseEvent("click", true, e.cancelable, window, 318153143, 
									  e.screenX, e.screenY, e.clientX, e.clientY, 
									  e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 
									  0, target.relatedTarget);
	
	pseudo_event_click.fakeClick=true;
	target.dispatchEvent(pseudo_event_md); 
	target.dispatchEvent(pseudo_event_mu); 
	target.dispatchEvent(pseudo_event_click);
	//log2("Dispatched fake click on: "+target.tagName+", "+target.id+", "+target.className+", "+target.textContent)
}

function ordinal(n) { // not cardinal
	var sfx = ["th","st","nd","rd"];
	var val = n%100;
	return n + (sfx[(val-20)%10] || sfx[val] || sfx[0]);
}

function updateValueTitles() {
	var just_inputs = getElementsByTagNames('input');
	for(var i=0; i<just_inputs.length; i++) {
		var title=just_inputs[i].title;
		title=title.replace(/Value:.*(?=Type:)/,"Value: "+just_inputs[i].value+".  "); //regexp "lookaround", not matching for replace /1
		just_inputs[i].title=title;
	}
}
function checkForInputs() {
	if (no_of_inputs != selects.length + rinputs.length + tareas.length) {
		no_of_inputs = selects.length + rinputs.length + tareas.length;
		var new_inputs = getElementsByTagNames('input,textarea,select');
		//log("checkForInputs "+new_inputs.length);
		var new_names={}, new_ones=[];
		text_inputs = inputs_on_page = 0;
		for(var i=0; i<new_inputs.length; i++) {
			var input=new_inputs[i];
			var name=input.name;
			var type=input.type;
			if (type=="checkbox") 
				name+=mdash_char+input.value;
			if ( ! name) {	name="yyNoName"+i; } //new_inputs[i].name=name; } ///RENAMEs
			new_names[name]=input;
			if ( input_names[input.name] )
				continue;

			new_ones.push(input);

			if (reddot) {
				//if (input.title.match(/Name:.*Value:.*Type/)) continue;
				// if ( !  ( /q|Gw/.test(name) && type=="text")  && ! /textarea/i.test(input.nodeName)) {
				//     if (type=="checkbox")  		  input.title+= "Name"+mdash_char+"value: "+name;
				//     else if (name)	    input.title+= "Name: "+name;
				//     else if (input.id)      	input.title+="Id: "+input.id;
				var val=input.value;
				// input.title+= ( /checkbox/.test(type) ? ""
				// 		    : ".  Value: " + ( val ?  
				// 				       ( input.type=="password" ? val[0] + "..." + val[val.length-1] : val)
				// 				  : null)
				// 		  );
				//input.title+=".  Type: "+type+".";
				if (/checkbox|radio/.test(type)) {
					// var w=parseInt(style.width), h = parseInt(style.height), min_size=18, hi_size=20;
					// //log("Size of tick "+h+"x"+w);
					// var fixed_sizes=getValue("fixed_sizes", false);
					// if ( (w < min_size || h < min_size) && ! fixed_sizes) {
					//   //log("Too small, set to "+min_size);
					//   input.style.setProperty("height", min_size+"px", "important"); 
					//   input.style.setProperty("width", min_size+"px", "important");
					//   input.style.setProperty("-moz-appearance", "none", "important");
					//   // input.addEventListener("mouseover", function(){ this.style.setProperty("height", hi_size+"px", "important");		  this.style.setProperty("width", hi_size+"px", "important");		}, false);
					//   // input.addEventListener("mouseout", function(){ this.style.setProperty("height", min_size+"px", "important");		  this.style.setProperty("width", min_size+"px", "important");		}, false);
					// }
					//var roll="";	    for(var j in style) roll+=j+":"+style[j]+"::"+style[style[j]]+", "
					//log("Computed style "+roll);
					// input.style.MozAppearance="none";
					// input.style.height="1em";
					// input.style.width="1em";
				}
				//} // if ! q|Gw
				//if (iframe) input.title+=" (Within iframe).";
				if (/text/.test(type))           	text_inputs++;
				if ( ! /hidden|submit|image|reset/.test(type) )           		inputs_on_page++;
			}
		}
		inputs=new_inputs;
		input_names=new_names;
		return new_ones;
	} //end if change in number of inputs
	else {
		var new_inputs = getElementsByTagNames('input,textarea,select');
		for(var i=0; i<new_inputs.length; i++) {
			var input=new_inputs[i];
			var name=input.name;
			var type=input.type;
			var style=window.document.defaultView.getComputedStyle(input, null)||{};
			if (style.display=="none" || style.visibility=="hidden")	continue;
			if (reddot) {
				// if (input.title.match(/Name:.*Value:.*Type/)) //update title value if necessary
				//input.title="";
				// if (type=="checkbox")  		  input.title+= "Name: "+mdash_char+"value: "+name;
				// else if (name)	    input.title+= "Name: "+name;
				// else if (input.id)      	input.title+="Id: "+input.id;
				// var val=input.value;
				// input.title+= ( /checkbox/.test(type) ? ""
				// 		    : ".  Value: " + ( val ? ( input.type=="password" ? val[0] + "..." + val[val.length-1] : val ) : null));
				// input.title+=".  Type: "+type+".";
				//if (iframe) input.title+=" (Within iframe).";
			}
		}
	}
} //end checkForInputs()

function prettyPrintUneval(obj, oneline) {
	var roll="",cnt=1; for (var i in obj) roll+=(ordinal(cnt++))+" field:"+"\t"+obj[i].v+"\t\t\n";
	return roll;
}
function ffver(){ // returns Firefox version number or -1.
		if (ffver.val) return ffver.val;
		ffver.val=navigator.userAgent.indexOf("Firefox/");
		if (ffver.val!=-1) ffver.val=parseInt(navigator.userAgent.substr(ffver.val+8));
		return ffver.val;
		//log("ffver ",ffver,document.URL,iframe);
}


async function editFormData() {
	await readPersistentData();
	if ( ! form_data && ! click) return;
	sprompt(widener+'\nThe following is the form filling information previously saved, '
			+ 'edit the values in the box at the bottom '
			+'(you may need to scroll down).'
			+'then Click OK, Click Cancel/Next to edit sites to which data applies or to simply cancel.\n\nKeep to the punctuation format.  '
			+'Do not edit the special characters, "'+bullet+'<tab>", leave them in place, unless '
			+'you want to remove or add an entire name/value pair.'
			+'\n\nNote.  Each input box or tick box on a webpage has an internal name, its value is what gets filled-in by the user.  '
			+'Pass the mouse over a form to see the internal names of the fields.'
			+'They are given in the form name:: value & must have a special character between pairs.  Checkbox names also have their default\nvalue appearing with their name after a dash ('+mdash_char+').\n\n' 
			+'To fill just the values without names, clear the input and type in\nthe values leaving a space between them.\nValues with spaces must be surrounded by double quotes.\n\n'
			+ chopLongString(bullet_tab+formDataToFromString(true)), formDataToFromString(false, undefined, "", true)
			, function(users_input) {    
				log("Got users_input "+users_input);
				if (users_input != null) {
					log("ip  "+users_input +" match: "+users_input.match(separator) );
					if (  users_input && ! users_input.match(separator)  )
						res=formDataToFromString(false, users_input, "name_value_paste");
					else
						res=formDataToFromString(false, users_input);
					if (res) {
						//log("Store "+uneval(res)+"\n\nAt page_key: "+page_key);
						page_object.form_data=res;
					} else  {
						delete page_object.form_data;
					}
					persistData();
				}
				else { //is null (ie, a cancel):
					log("Next, sprompt for window2");
					sprompt(widener+"\nThe following is the page(s) for which the form data and click information is valid."
							+"\nEdit the page name to indicate page name(s) where stored form data may be applied and click OK.\nClick CANCEL to leave as is."
							+"\n\nFor example, remove all but the site name to make it valid for the entire site or for a number of sites (can use regexp or a part of the name but no slashes)"
							, page_key, function(users_input) {
								log("From sprompt got:  "+users_input);
								if (users_input != null && page_key != users_input) {
									log(1);
									hash_host_list[users_input]=page_object;
									log(1);
									page_object=null;
								}
								persistData();
							} ); //result_handler end of function
				}//else
				
			} ); //result_handler end function
}

function editClickData(){
	sprompt(widener+"\nThe following is the XPath address of the auto-click element on this page."
			+"\nEdit it and click OK.\nClick CANCEL to leave as is.", (click?click.xpath:""),
			function(users_input) { 
				log("Got "+users_input+", click: "+click);
				if ( users_input != null ) {
					if ( ! click) click={};
					click.xpath=users_input;
					click.xxpath=users_input;
					persistData();
					console.log("Set click xpath to:"+click.xxpath+".");
				}
				else console.log("No input");
			});
}

function formDataToFromString(breaks, str_in, value_paste, isedit, tmp_data) {
	var data= form_data||{};
	if (tmp_data) data=tmp_data;
	var j=0, v, n, form_str="", extra_break="";
	tab=" "+bullet+" ";
	if (breaks) tab ="\n"+bullet_tab
	if ( str_in == undefined ) {  //toString
		for (n in data) {
			j++;
			v=data[n].v;
			if ( ! breaks ) {
				if ( ! v && v != 0)
					v="null";
				if (v==tick || v==ex) {
					data[n].checkbox=true;
					(v==tick ? v = "on":  v= "off");
				}
			}
			else { // breaks.
				if (typeof v == "object") {
					v = v + "\t\t[ "+getOptionsNames(n, v)+" ]"
					data[n].select_multi=true;
				}
				if (input_names[n] && input_names[n].type=="select-one")
					v = v  + "\t\t[ " + getOptionsNames(n , v) + " ]"
				if ( j % 3 == 0 ) extra_break="\n"
				else extra_break="";
			} // end ! breaks
			if ( ! (data[n].pw) || unhide || isedit) {
				form_str+=n+separator;
				form_str+=(breaks ? "\t" : " ") +v+(unhide?"(unhidden)":"")+extra_break+tab;
			}
			else    { 
				form_str+=n+separator
				this.pw=v;
				var i=0
				form_str += (breaks ? "\t" : " ") + (v[0]?v[0]:"");
				while (i++ < v.length-2)
					form_str+="*";
				form_str+=(v[i]?v[i]:"");
				form_str+=tab;
			}
		} //end for n in data
		return form_str.substring(0,form_str.length-tab.length);
	}
	//
	//endif str_in==undefined, hence it is fromString:
	if (value_paste) return doValuePaste(str_in, data);
	name_value=str_in.split(bullet);
	var val;
	for(var i=0; i < name_value.length; i++) {
		log("for loop, str_in, nm pair:"+name_value[i]);
		if ( ! name_value[i])
			continue;
		words=name_value[i].split(separator);
		if (words.length != 2)
			continue;
		var name=words[0].replace(/^\s+|\s+$/g,"");
		val = words[1].replace(bullet_regexp, "").replace(/^\s+|\s+$/g,"");
		if (val =="null")
			val=null;
		if ( ! data[name] ) data[name]=new Object();
		//if (data[name].pw)   continue;
		data[name].v=val;                       ////
		log("not pw, data[name] val: "+data[name].v);
		data[name].user_set=true;
		if (data[name].checkbox) {
			val=val.replace(/\s*/g,"");
			if (/^on$/i.test(val))
				data[name].v=tick;
			else
				data[name].v=ex;
		}
		
		if (input_names[name] && input_names[name].type=="select-one") 
			if (isNaN(val) ) 
				data[name].v = getOptionsIndex(input_names[name], val, data[name]);
		else
			data[name].v = val;
		log("Set val: "+data[name].v+", type "+(typeof data[name].v));

		if (data[name].select_multi) {
			data[name].v=val.split(/\s*,\s*/);
		}
	}
	for(var n in data) {
		if ( ! data[n].user_set) {
			delete data[n]
		}
		else {
			delete data[n].user_set
			delete data[n].checkbox;
			delete data[n].select_multi;
		}
	}
	if (countMembers(data) == 0)
		return 0;
	return data;
}

function doValuePaste(str_in, data) {
	str_in=stuffQuotedSpaces(str_in);
	log("Do paste str_in: "+str_in);
	var values=str_in.split(/\s+/);
	var val, name;
	for(var i=0; i < values.length; i++) {
		log("str in: "+values[i]);
		if ( ! values[i] )
			continue;
		val = values[i].replace(bullet_regexp, " ").replace(/^\s+|\s+$/g,"").replace(/[""]/g,"");//2 doubles for indent
		if (val =="null")
			val=null;
		name=nameAtI(data, i);
		log2(i+"th, val "+val+" name:" +name+ " "+(input_names[name]?input_names[name].type:"no ip"));
		if (name==false) continue;
		if ( ! data[name] ) data[name]=new Object();
		if (data[name].pw)   continue;
		data[name].v=val;                       ////
		data[name].user_set=true;
		if (input_names[name] && input_names[name].type=="select-one") {
			if (isNaN(val)) 
				data[name].v = getOptionsIndex(input_names[name], val, data[name]);
			else
				data[name].v = Number(val);
		}
		log2( "paste val: "+data[name].v+", typeof val "+(typeof data[name].v) );
		
		if (data[name].checkbox) {
			val=val.replace(/\s*/g,"");
			if (/^on$/i.test(val))
				data[name].v=tick;
			else
				data[name].v=ex;
		}
		if (data[name].select_multi)
			data[name].v=val.split(/\s*,\s*/);
	}	//matches end of for loop over values
	for(var n in data) {
		if ( ! data[n].user_set) {
			delete data[n]
		}
		else {
			delete data[n].user_set
			delete data[n].checkbox;
			delete data[n].select_multi;
		}
	}
	if (countMembers(data) == 0)
		return 0;
	return data;
} //actually matches end of doValuePaste()

function doNameValuePaste(str_in, data){
	name_value=str_in.split("\n");
	var val;
	for(var i=0; i < name_value.length; i++) {
		if ( ! name_value[i])
			continue;
		var words=name_value[i].split(":");
		if (words.length != 2)
			continue;
		var name=words[0].trim();
		val = words[1];
		if (val =="null")
			val=null;
		name=matchInputName(name, inputs);
		//log("Got name match of "+name+" data[name] :" +data[name]);
		if ( ! name ) continue;
		if ( ! data[name] ) data[name]=new Object();
		if (data[name].pw)   continue;
		data[name].v=val;                       ////
		data[name].user_set=true;
		if (data[name].checkbox) {
			val=val.replace(/\s*/g,"");
			if (/^on$/i.test(val))
				data[name].v=tick;
			else
				data[name].v=ex;
		}

		if (input_names[name].input_names[name].type=="select-one")
			if (isNaN(val) ) 
				data[name].v = getOptionsIndex(input_names[name], val, data[name]);
		else
			data[name].v = val;
		log("Paste set val: "+data[name].v+", type "+(typeof data[name].v));


		if (data[name].select_multi) {
			data[name].v=val.split(/\s*,\s*/);
		}
	}
	for(var n in data) {
		if ( ! data[n].user_set) {
			delete data[n]
		}
		else {
			delete data[n].user_set
			delete data[n].checkbox;
			delete data[n].select_multi;
		}
	}
	if (countMembers(data) == 0)
		return 0;
	return data;
}

function countMembers(obj) { var cnt=0;     for(var i in obj) if ( ! obj.hasOwnProperty || obj.hasOwnProperty(i)) cnt++; 	return cnt;    }

function matchInputName(name, data) {
	var res, ratio=0, high_ratio=0;
	for(var i in data) {
		ratio=ratioOfMatchedWords(name, data[i].name);
		//log("got ratio of "+ratio+" for "+name+" in data[i].name: "+data[i].name);
		if ( ratio > .5) {
			if (ratio > high_ratio) {
				high_ratio=ratio;
				res=data[i].name;
			}
		}
		if (ratio == 1)
			return data[i].name;
	}
	return res;
}

function nameAtI(obj, i) {
	var index=i;
	for(var j in obj) {
		if ( i == 0 ) return j || "yyNoName"+index;
		i--;
	}
	return false;
}

function  stuffQuotedSpaces(str) {
	var phrase_pos=0;
	var i=str.indexOf('"', phrase_pos);
	while (i !== -1) {
		var j=str.indexOf('"', i+1);
		var phrase=str.substring(i, j);
		str=stringCutAndPaste(str, phrase, phrase.replace(/\s/g, bullet), i);
		i=str.indexOf('"', j+1);
	}
	return str;
}

function stringCutAndPaste(str, tocutout, topaste, start) {
	var n=str.indexOf(tocutout, start||0);
	if (n != -1) 
		return str.substring(0,n) + ( topaste || "") + str.substring(n+tocutout.length); 
	return str;
}

function getOptionsNames(name, value_s) {
	var result="";
	var option_names=[], options;
	var ip=input_names[name];
	if (ip) options=ip.options;
	if (typeof value_s == "object") {
		if (ip && ip.type == "select-multiple" )
			for(var i in value_s)
				option_names[i]=getOptionsNames(name, value_s[i]);
		result =String(option_names).replace(/,\b/g,", ");
	}
	else {
		for (var i =0; i < options.length; i++) {
			//log("opt.inx: "+options.item(i).index +", look for: "+value_s +".  Opt.value: "+options.item(i).value );
			var option=options.item(i);
			if (option.index== value_s ) {
				if (option.textContent)
					result = option.textContent;
				else
					result = option.value;
			}
		}// end for.
	}
	return result.replace(/[\r\n]/g, "").replace(/^\s+|\s+$/g,"");
}

function getOptionsIndex(ip , in_val, name) {
	var index = 0, best_match = 0;
	for (var i =0; i < ip.options.length; i++) {
		var option=ip.options.item(i);
		var opts_val=option.value;
		if ( option.textContent && option.textContent.trim() )
			opts_val=option.textContent;
		opts_val=opts_val.toLowerCase().trim();
		in_val=in_val.toLowerCase().trim();
		var match_ratio=ratioOfMatchedWords(in_val, opts_val);
		if (match_ratio > best_match ) {
			best_match=match_ratio;
			name.str=opts_val;
			//index=String(option.index);
			index=option.index;
			if (match_ratio > 0.9) 
				//	    return String(option.index);
				return option.index;
		}
	}
	return index;
}

function ratioOfMatchedWords(source_words, target_words) {
	source_words=source_words.replace(/[^a-zA-Z0-9_ \t]+/g," ").split(/\s+/);
	var matches=0, ratio=0, targ_ratio=0;
	for(var i in source_words)
		if (target_words.match( RegExp(source_words[i],"i") ))
			matches++;
	ratio = matches/source_words.length;
	targ_ratio = matches/target_words.replace(/[^a-zA-Z0-9_ \t]+/g," ").split(/\s+/).length;
	ratio = (ratio + targ_ratio) / 2;
	//log("opt match "+source_words+", in targ "+target_words+".  # Matches "+matches+", in "+source_words.length+" , targ ratio:, "+targ_ratio+" ratio "+ratio)
	return ratio;
} 

function parseTextarea(ta) {
	var result="";
	var lines=ta.value.trim();
	//lines=lines.replace(/[\r\n]\s*[\n\r]/g, "\n"); //removes empty lines
	//log("lines:\n "+uneval(lines));
	lines=lines.split(/\n/); // check MSDOz!!
	lines=reorderLines(lines);
	for(var i=0; i < lines.length; i++) {
		var val, res;
		var line=lines[i].trim();
		if (line && ! /:/.test(line))
			line=": "+line;
		if (/::/.test(line) || multiMarked(i) ) {
			val=line.splitOnce(/\s*:+\s*/)[1];
			if (val) result += " " + parseOptionalValues(val, i);
		}
		else {
			val=line.splitOnce(/:\s*/)[1];
			if (res=parseDateTime(val, i)) result += " " + res;
			//	  else if (val) result += ' "'+val.replace(/[\x27\x22]/g,"")+'"';
			else if (val) result += ' "'+val.replace(/[\x22]/g,"").replace(/[\x27]/g,"\\\x27")+'"';
			else result += " null ";
		}
	}
	return result.trim();
}

// setValue("line_order", "3*, 1, 2DsT, 4");
// setValue("optionals", "line1word2of3");

function parseDateTime(val, i) {
	var datetime=gLine_order, date="", time=""; //eg, "3*, 1, 2DsT, 4"
	
	if ( ! /[DT]/i.test(datetime))
		return null;
	datetime=datetime.split(/,/)[i];
	if ( ! /[DT]/i.test(datetime))
		return null;
	if (/Ds/i.test(datetime)) 
		date=new Date(val).slashFormat();
	if (/T/i.test(datetime)) {
		time=new Date(val).ampmFormat();
		time += " " + getTimeZone();
	}
	result = date+" "+time;
	if (/TD/.test(datetime))  result = time+" "+date;
	//log("ret date/time "+result);
	return result;
}

function parseOptionalValues(val, i) {
	var opts=gOptionals; //getValue("optionals", ""), opt_word, max_words;
	var pos=opts.indexOf( "line" + (i+1) ); //line number not zero counted.
	if (pos != -1) {
		pos= pos + ("line"+i).length + 4;//eg, "line1word2of3"
		opt_word=parseInt ( opts.substr (pos) );
		pos += (opt_word+"").length + 2;
		max_words=parseInt ( opts.substr(pos) );
		//log(pos+ "<pos.  opt_word "+opt_word+", max "+max_words);
		var words=val.split(/\s+/);
		if (words.length < max_words) {
			words.splice(opt_word-1, 0, "null"); //insert 'null' if optional is not given.
		}
		val=words.join(" ");
	}
	return val;
}

function multiMarked(i) {
	//	var marked=getValue('line_order', "1, 2, 3, 4, 5");
	var marked=gLine_order || "1, 2, 3, 4, 5";
	marked=marked.split(/,/);
	return /\*/.test(marked[i]);
}

//greasemonkey.scriptvals.userscripts.org/Simple Form Saver.line_order
// No longer, uses GM menu cmd getSetConfigs()
function reorderLines(lines){
	//	order=getValue('line_order', "1, 2, 3, 4, 5").replace(/[^\d,]/g,"");
	order=(gLine_order || "1, 2, 3, 4, 5").replace(/[^\d,]/g,"");
	order=eval("["+order+"]");
	var newlines=[];
	for( var i in lines)
		if (i  <  order.length && order[i] != 0) {
			var old_pos=order[i]-1;
			if (old_pos < lines.length)
				newlines[i]=lines[old_pos];
			else
				newlines[i]="null";
		}
	else
		newlines[i]=lines[i];
	return newlines;
}

function pasteIntoForm() {
	// 1. Open a window containing help info with a list of form field names and into which user can paste form values, then 
	// 2. Label all form elements in page with a visible sequence number, then
	// 3. Set up a click handler for user's 'OK', which
	// 4. Calls parseTextarea() and doValuePaste() to set up values given by user, then
	// 5. Ask user to confirm the values to be set, finally call fillForm() with ordered array tmp_data.

	log2("pasteIntoForm() "+typeof pasteWindow);
	//readPersistentData();
	if (special_ta) log("special_ta tc "+special_ta.value+" id "+special_ta.id);
	if (getById("SimpleFormSave")) return;
	var ta_val="";
	//var pasteWindow=uwin.open("ex_form.html#bizarreoKeyer","_blank", "foreground");
	try { if (pasteWindow && !pasteWindow.closed) {  // in order to focus on pasteWindow
		var texta = pasteWindow.div.getElementsByTagName("textarea")[0];
		ta_val=texta.value;
		log2("ta_val "+ta_val);
		pasteWindow.close();
	}}  catch(e) { pasteWindow=null; } //catch occurs if window was closed, cant acess dead object.

	if (plat_chrome) pasteWindow=window;
	else pasteWindow=unsafeWindow.open("","pasteIntoForm","menubar=no,scrollbars,location=no,toolbar=no, status=no,left="+screen.width+"px,top="+(screen.height/2-300)+"px,width=600em");
	log("pasteWindow "+pasteWindow);
	//pasteWindow.document.addEventListener("keydown", function(e) { if (e.keyCode == 27)  pasteWindow.sfspasteCANCEL=true;}, 0);
	var div=pasteWindow.document.createElement("div"); div.id="SimpleFormSave";
	div.setAttribute("onkeydown","if (event.keyCode == 27)  window.sfspasteCANCEL=true;");
	pasteWindow.div=div;
	var body=pasteWindow.document.body;
	function listFields() { var skip, roll="field(s) named, "; 
							for (var i=0; i < inputs.length; i++) {
								var ip=inputs[i];
								if(skip==ip.name) continue;
								if (ip.type=="radio") skip=ip.name;
								var style=window.document.defaultView.getComputedStyle(ip, null)||{};
								var visibility=( (style.display=="none" || style.visibility=="hidden") ? " (invisible)" : "");
								ip.visibility=visibility;
								roll+= ip.name+visibility+", ";
							}
							return roll; }
	var tatitle="For an example form with three fields: name, last-name, sex,\npaste-in as follows in three lines,\nJohn\nWebster\nmale"
		+"\nTo do it in just one line use two colons, type or paste,\n::John Webster male\nIf there was also a field for the middle name to set it as null put:\n::John null Webster male"
		+"\nIf one of the values after the double colon (::) had spaces in it, it would need to be surrounded by double quotes, eg,\n::John "+'"Boyle OConnor Fitzmaurice Tisdall OFarell"'+" male.  "
		+"\nIf you know the names of the inputs or can guess or check them by looking at the HTML of the page then you could use, eg, "+"\nlastname: Smith"+", on any line and it would fill it in the right field";
	div.innerHTML="<p>Enter or Paste form field values into the box directly below this text; do it for each field of the form.  Click OK when complete, or leave blank and click OK to see current values.  You can put a colon "
		+"before the value if desired.  You may need to maintain the same order as the page's form so fields are now numbered with '#' on the webpage, watch out for invisible (red) fields.  "
		+"<br><br>Use a new line for each field, or use double colons (::) to put more than one field's values on a single line.  "
		+"Put the mouse over this and below for instructions and examples.  You can put the mouse over the fields of the form to see details of form.<br><br>The page has "+listFields()+"</p>"
		+"<textarea style='width:100%;height:20em;  border: double;background-color:#fffff4;color:midnightblue;' title='"+tatitle+"';>"
		+(special_ta?special_ta.value:ta_val)+"</textarea>";
	var ta=div.getElementsByTagName("textarea")[0]; 
	div.innerHTML+="<form>"
		+"<input type=button value='Cancel/Next' style='width:100; height:50;font-size: 12' "
		+"onclick='window.sfspasteCANCEL=true' " //workaround for window perms problem.
		+">"
		+"<input type=button value='OK' style='width:100; height:50;font-size: 24;' "
		+"onclick='window.sfspasteOK=true' "
		+"></form>";
	div.firstElementChild.title='Give one value on each line eg, "Pepsi" or put them in the form name \u2039colon\u203a value, eg, "Company: Pepsi" or indeed just \u2039colon\u203a value, eg, ":Pepsi". For fields not to be filled-in at all just put a single colon on that line.  '
		+"If names are given the name need not match the real name; each value is copied into the approriate field based not on the name but on the field ordering"
		+" unless the config value for pasting names is set in "
		+"about:config.  The first line is copied to the first form field, etc..  One field value per line or,"
		+" to allow more values on one line, use double colons for contiguous fields, eg, 'time:: 12:23 Eastern' will fill-in two fields, the time and the"
		+" time zone.  To skip a field use the value 'null' or leave the part after the colon empty.  Hover over below to see help text.";
	//ta.value="\n  ";
	div.innerHTML+=""
		+"<input title='When above text box is empty click Prev./Save to paste values that were previously saved here in this dialog.  Or, when not empty, "
		+"click it to save the values currently in above text box of this dialog.  To get current values, click OK when empty, to see current values that are set in the form and choice values/names middle click on red dot.' type=button value='Prev./Save' style='width:100; height:50;font-size: 12' "
		+"onmouseup='window.sfspastePrev=event.which' "
		+">";
	log2("prevPasteVals:"+page_object.prevPasteVals+".");
	if (page_object.prevPasteVals) 
		div.innerHTML+="<div id=prevsave>Previously saved at this dialog:<br><pre>"+page_object.prevPasteVals+"</pre></div>"
	else
		div.innerHTML+="<div id=prevsave></div>"
	
	ta.value=ta_val; //set from innerHTML not here!
	log2("set ta_val "+ta.value);
	var title=div.firstElementChild;
	body.appendChild(div);
	if (plat_chrome) {
		div.style.cssText=" z-index: 99; top: 40px; height: 50%; opacity: 0.85; background-color: white; font-family: Helvetica; font-weight: bold; font-size: small; padding: 10px";
		ta.style.cssText="width: 50%; margin: 0;  border: none;"
			+" -moz-appearance: none; border-left: double; min-height: 300px; ";
		title.style.cssText=	"background-color: -moz-field; margin:0; border: double;";
		log("set cssTExt "+ta.style.cssText);
	}
	else {
		div.style.cssText=" top: 40px; height: 50%; font-family: Helvetica; font-weight: bold; font-size: small; padding: 10px";
		// ta.style.cssText="width: 50%; height: 150px; margin: 0;  border: none;"
		// 	+" -moz-appearance: none; border-left: double;"
		title.style.cssText=	"background-color: -moz-field; color: -moz-FieldText; margin:0; border: double;";
	}
	//title.scrollIntoView();
	//pasteWindow.resizeTo(body.offsetWidth+50,body.offsetHeight+100);
	var labels=[], radio_names={}, detract=0; 
	for(i=0; i < inputs.length; i++) {
		var input=inputs[i];
		var l=document.createElement("label");
		var prev=input.previousSibling;
		if ( prev && prev.tagName=="LABEL" && /#\d$/.test (prev.textContent) ) continue;
		input.parentNode.insertBefore(l, input);
		if (input.visibility) { l.title="Invisible input, provide a value or leave line blank, name="+input.name; l.style.color="red";l.style.backgroundColor="black";}
		if (input.type=="radio") {
			if (radio_names[input.name]==undefined)
				radio_names[input.name]=(i+1-detract);
			else detract++;
			l.textContent=" #"+(radio_names[input.name]);
		}
		else l.textContent=" #"+(i+1-detract);
		labels.push(l);
	}
	var processTextarea=function() {
		var ta = div.getElementsByTagName("textarea")[0];
		var str_in, parse_method=gParse_method; //can be set to "names", default is "values" which uses the order to know which value to paste.
		if (parse_method[0] != "n") 	  parse_method=true; else parse_method=false;
		if (ta.value=="") { fillWithCurrentValues(ta); return -1; }
		if (parse_method)
			str_in=parseTextarea(ta);
		else str_in=ta.value.trim();
		//log("parse_method "+parse_method+", after parse, str_in: "+str_in);
		var data={};
		for(i=0; i < inputs.length; i++) {
			var input=inputs[i], name=input.name;
			data[name]={};
			data[name].i=i;
			if (input.type=="checkbox") {
				data[name].checkbox=true;
				var cbname=name+mdash_char+input.value;
				data[cbname]=data[name];
				delete data[name];
			}
		}
		var res;
		if (parse_method)
			res=doValuePaste(str_in, data), list=widener, j=0; 
		else
			res=doNameValuePaste(str_in, data), list=widener, j=0; 
		//console.log("Paste, res: ",res," data.len: "+data.length);
		for (var i in data) {
			if (typeof data[i] === 'function') continue;
			//log2("for i in data.  i:"+i+"="+data[i]+", type: "+typeof data[i]);
			if (parse_method)
				list +=  "" //"field #"  + ( ( j++ ) + 1) 
				+"To be filled-in: "+data[i].v+" \t\t ("+i+")\n";
			//		+"To be filled-in: "+(data[i].str ? data[i].str : (data[i].v||"") )+" \t\t ("+(inputs[j-1].name||"")+")\n";
			else 
				list +=   "input name: "+i
				+", to be filled-in with: "+(data[i].str ? data[i].str : (data[i].v||"") )+"\n";
		}
		list=list.replace(/\\\x27/g,"\x27");
		//      var go_ahead=
 		var w=pasteWindow.outerWidth, h=pasteWindow.outerHeight;
		//pasteWindow.resizeTo(1,1); 
		//window.focus();
		//log2("w h "+pasteWindow.outerWidth+" "+pasteWindow.outerHeight);
		sconfirm(list+"\n\nConfirm or Cancel", function(go_ahead) {
			if (go_ahead) {
				if (res) fillForm(false, false, res);
				inOutSet();
			}
			if (plat_chrome) {
				div.style.display="none";
				div.parentNode.removeChild(div);
				div=null;
			}
			for (var i in labels) {
				var parent=labels[i].parentNode;
				if (parent) parent.removeChild(labels[i]);
			}
			//pasteWindow.resizeTo(w, h);
			pasteWindow.focus();
		}); //sconfirm()
		
	}; // end processTextarea() ///////////////////////////////
	
	var form_inputs=div.getElementsByTagName("input");
	form_inputs[0].do_onclick=function() {  // was "onclick" workaround window perms problem.
		pasteWindow.close(); log("cancel");	    
		for (var i in labels)
			if (labels[i].parentNode) labels[i].parentNode.removeChild(labels[i]);
		if (plat_chrome) {
			div.style.display="none";
			div.parentNode.removeChild(div);
			div=null;
		}
	};
	form_inputs[0].style.cssFloat="left";
	form_inputs[1].do_onclick=function (){
		return processTextarea();                          ///////////////////////
		//log2("processTextarea "+ta.value+".");
	};
	ta.focus();
	pasteWindow.focus();
	if (form_inputs[2])
		form_inputs[2].do_onclick=async function (which_button) {
			log2("Which_button "+which_button);
			var ta = div.getElementsByTagName("textarea")[0];
			var taval=ta.value.trim();
			if (taval=="") { ta.value=page_object.prevPasteVals; }
			else {
				await readPersistentData("listfs");
				page_object.prevPasteVals=taval;
				var paradiv=$(div).find("#prevsave");
				paradiv.html("Previous saved at this dialog:<br><pre>"+page_object.prevPasteVals+"</pre>");
				persistData();
				//alert("Saved "+page_object.prevPasteVals)
			}
		};
	log2("was set ta_val "+ta.value);
	pasteWindow.ta=ta;
	pasteWindow.setTimeout(function() { ta.focus(); }, 3000);

	var siid=setInterval(function() { //Added in cos of new window's lack of perms to run code in this file.
		try { if (pasteWindow.closed) clearInterval(siid); var tmp=pasteWindow.sfspasteOK; } catch(e) { clearInterval(siid);}
		if (pasteWindow.sfspasteOK) {
			pasteWindow.sfspasteOK=false;
			if (form_inputs[1].do_onclick() != -1) {
				pasteWindow.blur();
				window.focus();
			}
		}
		if (pasteWindow.sfspasteCANCEL) {
			clearInterval(siid);
			pasteWindow.sfspasteCANCEL=false;
			form_inputs[0].do_onclick();
			pasteWindow.close();   //cancel
		}
		if (pasteWindow.sfspastePrev) {
			// tbd, paste prev values or save existing ones.
			form_inputs[2].do_onclick(pasteWindow.sfspastePrev); //see page_object.prevPasteVals
			pasteWindow.sfspastePrev=false;
		}
	}, 200);
} // end pasteIntoForm()


function fillWithCurrentValues(ta) { // Fills the given textarea with saved form values.  Happens when clicking ok when pasting values and empty fills with current values.
	var tmp_data=saveFormData(true);
	var str_in=formDataToFromString(false, undefined, false, false, tmp_data);
	var vals=str_in.split(separator);  //separator is "::" w/ ctrl char betwixt.
	var roll="";
	for (var i=1; i< vals.length;i++) {
		roll+=vals[i].split(bullet)[0].trim()+"\n"; //bullet is ⚫ 
	}
	ta.value=roll; //+"\n\n";
}

function confirmClickRecording() {
	sconfirm("The next part of the webpage on which you click shall be recorded and\ncan be clicked again on subsequent visits to this page, automatically or manually."
			 +"\n\nTo ensure you click on the correct element all elements can be\nhighlighted with a red outline & show XPath (it also appears in the console,\nsee status bar (if allowed by about:config, see userscript webpage) for element tag."
			 +"\n\nClick OK to proceed\n\nHit Cancel/Next to hightlight elements.\nNote, highlighted elements may move position, hit 'Esc' key to clear highlighting."
			 +"\n\nThen click on chosen element to have a click replayed on this page when visited in future.\nShift-click searches for nearest input, form, link, or onclick element.  "
			 //+"\n\nInstead, to have an element permanently deleted from the page, do, control-shift-click, on the element.\nRecording a click will not involve a real click on the page."
			 , function(reply) { //result handler function
				 recording=reply;
				 if ( recording == false) {
					 GM_addStyle("* { border-color: red ! important ; border-width: 1px ! important ; border-style: solid ! important ; } ");
					 window.document.body.addEventListener("mouseover", mouseOver, false);
					 addEventListener("keyup", function(e) {
						 if (e.keyCode!=27) return; //esc
						 var headNode=window.document.getElementsByTagName("head")[0];
						 headNode.removeChild(headNode.lastElementChild);
						 window.document.body.removeEventListener("mouseover", mouseOver, false);
						 window.document.body.removeEventListener("keyup", arguments.callee, false);
					 }, 0);
				 }
				 //window.document.body.addEventListener("click", recordClick, false);
				 window.document.body.addEventListener("click", recordClick, true); //capture mode
				 window.status="Shall save next click on this page: "+page_key;
				 wait_for_click=true;
			 })
}

function recordClick(e) {
	log("recordClick "+e.target.className+" "+e.target.tagName+" "+recording);
	if ( $(e.target).hasClass("sfsdiax") ) return true;
	if ( recording === undefined) return true;
	// if ( ! FireFox && ! window.counted_confirm_click) { 
	//   window.counted_confirm_click=true;
	//   return true;
	// }
	wait_for_click=false;
	if (e.preventDefault) {
		e.preventDefault();   
		e.stopPropagation();
		e.stopImmediatePropagation();
	}
	if ( recording == false) {
		var headNode=window.document.getElementsByTagName("head")[0];
		headNode.removeChild(headNode.lastElementChild);
		window.document.body.removeEventListener("mouseover", mouseOver, false);
	}
	this.removeEventListener("click", arguments.callee, true);
	//img.inOutSet(); might be on different page, so cant access this img.style w/o security  error
	//readPersistentData();
	var click={};
	var elem=e.target;
	var cond=false, onclick;
	var orig_onclick=elem.getAttribute("onclick"); // or onmousedown, up
	if (e.ctrlKey && e.shiftKey)
		click.ctrlShift=true;
	else {
		if (e.shiftKey) {
			while ( elem && elem.tagName ) {
				onclick=elem.getAttribute("onclick");
				cond= /^(input|form|a)$/.test ( elem.tagName.toLowerCase() );
				cond = cond || onclick;
				if (cond) break;
				elem=elem.parentNode;
			}
			if ( ! elem || ! elem.tagName)
				elem=e.target;
		}
		click.onclick_js=elem.getAttribute("onclick") 
		click.href=elem.href;
	}
	click.own_href=href;
	click.xpath=getXPath(elem, true);
	click.id=elem.id;
	click.className=elem.className;
	click.xxpath=getXPath(elem);
	//log("Click was on element, innerHTML: "+elem.innerHTML);
	elem.id=null;
	//elem.parentNode.removeChild(elem);
	page_object.click=click;
	var click_msg="Mouse click to save was on element: "+elem.tagName+", text, "+elem.textContent+", class, "+elem.className+" path: "+click.xpath;
	console.log(click_msg+" for page "+page_key);
	var subpage=href.substring ( href.lastIndexOf ( page_key ) + page_key.length)
	//log("confirm again on subpage: "+subpage);
	if (click.ctrlShift) // async calls, events can intervene (eg, unload and clobber data)
		sconfirm( "Page name: "+page_key+"\n" +"Element xpath: " + click.xpath+(click.xpath!=click.xxpath ? " ("+click.xxpath+")":"")+widener
				  +"\n\nThe element on which the mouse click was just made shall be automatically deleted from this page from now on, "
				  +"unless automatic form-fill & replay are explicitly disabled for the page." 
				  +"\n\n"
				  + "Click 'OK' to auto-delete this element in future on this specific page name, even when the page name is  suffixed."
				  +"\n\nClick 'Cancel ' to abort or to specify on which pages the element is to be deleted"
				  +(subpage  ?  ", ie, on entire site or only on pages ending with the suffix:\n\t\t" + subpage : "" )  + "."
				  +"\n\n"
				  +(suspend_replay ? "\n\nNOTE: auto/replay-delete is currently suspended, to enable it go to the toggle in the GM menu." : ""),
				  reply_handler
				);
	else
		sconfirm( "Page name: "+page_key+"\n" + "Element xpaths: " + click.xpath+(click.xpath!=click.xxpath ? "\n\n("+click.xxpath+")":"")+"\n"+elem.textContent.replace(/\n/g,"")+widener
				  +"\n\nThe mouse click just made shall be replayed automatically on this page from now on " 
				  +"unless automatic form-fill & replay are explicitly disabled for the page or in general.  " 
				  +"\nAny form data for the page will also be filled in prior to replaying the click unless auto-fill has been explicitly disabled for this page."
				  +"\n\nTo access this page in future it may be necessary to first select GM icon 'User Script Commands' menu option 'Suspend the Auto-Replay' or explicitly disable auto-replay for the page so click will only replay when the form is manually filled in via the red icon at top left or the GM icon"
				  +"\n\n"
				  + "Click 'OK' to auto-replay this click in future on this specific page name, even when the page name is suffixed."
				  +"\n\nClick 'Cancel/Next ' to specify on which page(s) to replay it, "
				  +(subpage  ?  ", ie, on entire site or only on this page ending with the suffix:\n\t\t" + subpage : "" )  + ", or to abort."
				  +""
				  +(suspend_replay ? "\n\nNOTE: auto-replay is currently suspended in general, to enable it go to the toggle in the GM menu." : ""),
				  reply_handler
				);
	function reply_handler(reply) {
		//log("reply_handler for record "+reply);
		if (reply) {
			page_object.click=click;
			if(page_object.automatic!=false) page_object.automatic="on";
			click.own_href="any";
			recording=undefined;
			hash_host_list.msg=click_msg
			persistData();
			window.status=": "+hash_host_list.msg;
		}
		else { //cancel
			sprompt("Will replay click (or element deletion) on the following pages.  "
					+"\nThe site is, "+site+", edit the details as required or delete all but "
					+"the site name for it to operate on entire site."
					+"\nForward slashes[/] after the site name specifies"
					+"\nthat it only applies to that page not whole site. "
					+"\n\nClick 'Cancel' to abort entire recording.", decodeURIComponent(page_key+subpage), 
					function(subpage_reply)	{ 
						if (subpage_reply) {
							log("page_key "+page_key+", == reply?: "+subpage_reply);
							if (page_key != subpage_reply) { //move all to new page object, ie, & del this one. 
								click.own_href="any"; 
								var po=hash_host_list[subpage_reply];
								console.log("New page object for, "+subpage_reply+", previous, "+page_key);
								if ( ! po) { 
									po=new Object(); 
									function copy(destination, source) {
										for (var property in source) {
											if (typeof source[property] === "object" &&
												source[property] !== null ) {
												destination[property] = destination[property] || {};
												arguments.callee(destination[property], source[property]);
											} else {
												destination[property] = source[property];
											}
										}
										return destination;
									};
									copy(po, page_object);
									log("no page for it, copied page_object "+page_object+" to po");
								}// end if !po
								
								hash_host_list[subpage_reply]=po;
								po.click=click; 
								if(po.automatic!=false) po.automatic=true;
								//page_object={};
								log("cleared page obj and set hash_host_list of given to "+hash_host_list[subpage_reply]);
								console.log("Operates on different pages: "+subpage_reply);
							} //end if != subpage_reply
							else { 
								page_object.click=click;
								if(page_object.automatic!=false) page_object.automatic=true;
								click.own_href="any";
								log(" == to page_key, own_href: "+click.own_href)
							}
						}
						else { window.status="Click recording aborted"; console.log("Recoding aborted"); return;}
						persistData();
						recording=undefined;
					});
		}// end else //cancel
		
	} // end reply handler
} // end recordClick()




function mouseOver(e) {
	var msg="Mouse is over: "+e.target.nodeName+".   Xpath: "+getXPath(e.target);
	window.status=msg;
	console.log( msg+"\n"+e.target.textContent.substr(0,40).replace(/\n+/g," ") );
}

function handlePopups(e) { try {     //node was inserted
	if (!e.target.tagName) return;
	if (recording !== undefined) return;
	setTimeout(async function() { await readPersistentData("popups");   },  0);
	var target=e.target;
	if (page_object.automatic) {
		var opt_flag;
		var new_elems=getElementsByTagNames("input,textarea,select"
											+(waiting_for_an_option ? ",option" : "") , target);
		var news=new_elems.slice(0);
		while ( target=new_elems.pop() ) {
			if ( old_elems.some(function(e) {return target==e; }) ) continue;
			if (target.tagName.toLowerCase()=="option") { opt_flag=target; target=target.parentNode;}
			if (/^(input|textarea|select)$/.test(target.tagName.toLowerCase()))
				if (form_data && form_data[target.name] ) {
					var once_off=[];
					once_off[0]=target;
					if (target.options) {
						var index=form_data[target.name].v;
						if (target.options.length <= index) { waiting_for_an_option = target; continue }
						if (opt_flag && opt_flag.index!=index) continue;
						if (waiting_for_an_option == target) waiting_for_an_option=false;
					}
					setTimeout(function() {      fillForm(once_off) }, 0);
					//target.title  +=  (   target.name  ?   "Name: "   +   target.name    :      (target.id ?   "Id: "   +  target.id    :  ""   )    )  +     ".  Value: "  +  target.value  +   ".  Type: "   +    target.type
				}
		} // end while
		old_elems=news.length? news : old_elems;
	}
	if (click && ! waiting_for_an_option && no_element_for_click && ! suspend_replay) {
		if (getXPathElem()) { 
			ws(function() {  	if (no_element_for_click) {    if (fillForm(false, true)) no_element_for_click=false;    }  	    }, 0);
		}
	}
}catch(e){console.log(e); throw(e);}}

function getXY(obj) {
	var curleft = 0;  var curtop = obj.offsetHeight + 5;  var border;
	function getStyle(obj, prop) {	    return document.defaultView.getComputedStyle(obj,null).getPropertyValue(prop);    }
	if (obj.offsetParent)    {
		do	{
			//  If the element is position: relative we have to add borderWidth
			if (  /^rel/.test ( getStyle (obj, 'position') )  ) {
				if (border = getStyle(obj, 'border-top-width')) curtop += parseInt(border);
				if (border = getStyle(obj, 'border-left-width')) curleft += parseInt(border);
			}
			curleft += obj.offsetLeft;
			curtop += obj.offsetTop;
		}
		while (obj = obj.offsetParent)
	}
	else if (obj.x)    {
		curleft += obj.x;
		curtop += obj.y;
	}
	return {'x': curleft, 'y': curtop};
}

function trickleUp(el) { try {
	var style=window.document.defaultView.getComputedStyle(el, null);
	//if (/^rel/.test(style.position))     el.style.setProperty("position","static", "important");
	if (/^abs/.test(style.position))  {   el.style.setProperty("margin-left","0px", "important"); 
										  el.style.setProperty("margin-right","0px", "important"); 
										  log("trickleUp unset abs, margins, on "+el.tagName+" "+el.id);
									  }
	if (el.parentNode) trickleUp(el.parentNode); } catch(e){}
					   }

function getTimeZone() {
	var right_now = new Date();
	var jan_here = new Date(right_now.getFullYear(), 0, 1, 0, 0, 0, 0);  
	var temp = jan_here.toGMTString();
	var jan_GMT = new Date(temp.substring(0, temp.lastIndexOf(" ")-1));
	var offset =  (jan_GMT - jan_here) / (1000 * 60 * 60);
	offset|=0;
	//log("off set "+offset);
	switch(offset) {
	case 0: return "GMT";
	case 5: return "eastern";
	case 6: return "central";
	case 7: return "mountain";
	case 8: return "pacific";
	default: return "GMT+"+-offset;
	}
}

function getById(id) {
	var el=window.document.getElementById(id);
	return el;
}

function getByIdOrClass() {
	var res=[];
	for(var i=0; i<arguments.length; i++) {
		var els=[];
		els[0]=window.document.getElementById( arguments[i] );
		if ( ! els[0] )  els=window.document.getElementsByClassName( arguments[i] ); 
		for (var j=0; j < els.length; j++) {
			log(arguments[i]+"<len: "+els.length+" push "+els[j].tagName+", id: "+els[j].id+", class "+els[j].className);
			res.push(els[j]);
		}
	}
	return res;
}

function esc(str) {
	return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

function trimchars(str,charset) { return str.replace( RegExp("^["+charset+"]*|["+charset+"]*$", "g" ) , "");}
// unbrackets etc, removes charset from either end of string. eg, trimchars("(astring)","()");

scrollToMid=function(elem) {
	console.log("scrollToMid",elem);
	var pos=getXY(elem);
	var  midY=window.innerHeight/2|0, midX=window.innerWidth/2|0;
	//log2("mid "+midX+" "+midY+", scrollPos:"+window.scrollX +" "+window.scrollY+". Elem pos: "+uneval(pos));
	pos.x -= window.scrollX+midX; pos.y -= window.scrollY+midY;
	scrollBy(pos.x, pos.y);
}

async function exportImport() { 
	log("Export/Import called at "+location.href);
	var data;
	data=await getValue(hostFormsList,{}); 
	log("export "+typeof data);
	data=stringify(data);
	sprompt("Warning, clicking OK will set the entire form "
			+"data to the values below.  The existing form data so far stored "
			+"given below and can be copied and pasted to the same export/import window on "
			+"another browser, or backed up to a text file and restored later on in this same browser.  "
			+"Or, if you risk it, you can even edit the form data and click OK to save it, for example if "
			+"the host/website name has changed.  If userProfiles set you may also need to switch profile and export."
			+"\nUserscript Simple Form Saver.\n\n",
			data?data:"", 
			async function(reply) {
				if (reply==null) { log("rnull"); return;}
				log("reply type: "+typeof reply);
				if (reply) log("40 init chars: "+reply.substr(0,40)+" reply len: "+reply.length);
				reply=trimchars(reply,"()"); // rm () if old uneval was used to make previous export.
				//translate 2xUnevals (an FF write) to 2xJSON.stringifys (a chrome write)
				//on chrome, read is JSON parse followed by an eval, FF is 2xeval.  One of each is in callees other is in GM_*Value wrappers below.  Redef of uneval on chrome.
				//log("eval "+eval);
				try {
					if (plat_chrome) { 
						reply=parse(reply);
						log(" JSON res:"+reply);
					}
					else reply=parse(reply);
				}catch(e) {console.log("Caught error: "+e);}
				log("Ex/In Got parsed object "+reply);
				//}
				setValue(hostFormsList, reply);
				console.log("readPersistentData...");
				await readPersistentData("export");
			});
}

function unhidePasswords() {
	//showHiddenElements();
	var pws = document.querySelectorAll('input[type="password"]');
	unhide="onceoff";
	for (var i=0; i < pws.length; i++) {
		var inp=pws[i];
		inp.form.autocomplete="off";
		inp.type="text";
		moveCaretToEnd(inp);
		if (inp.form) {
			inp.form.addEventListener("mousedown", function(e) {
				inp.type="password";
				setTimeout(function(){ inp.type="text";inp.focus();moveCaretToEnd(inp);},500);
			}, true);
		}
	}
}
//sprompt("abc","", function(v){alert("value:"+v+". type:"+typeof v)});

function getSetConfigs() {
	sprompt("Enter config value name below.\nSee this script's page at openuserjs.org websites for meaning.  "
			+"\nAvailable config names are:"
			+"\nff58iframe_fix"
			+"\npaste_shortcut"
			+"\nline_order"
			+"\noptionals"
			+"\nparse_method"
			,"",async function(configName){
				if (configName != null){
					var val=await getValue(configName,"");
					sprompt("Current value is given here, enter new value to change it:", val, async function(configValue){
						if (configValue==null) return;
						if(/true|false/.test(configValue)) configValue=eval(configValue);
						await setValue(configName, configValue);
						alert("Set "+configName+" for "+scriptName+" to:"+configValue);
						init_configVars(); // re-reads into global vars the config vals
					});
				}
			});
}
function showHiddenAncestors(el) {
	var ancestors=el.parents();
	ancestors=ancestors.add(el);
	ancestors.each(function(){this.type="";});
	ancestors.each(function(){$(this).css("visibility","visible"); $(this).css("opacity","1"); });
	
	ancestors.each(function(){
		if (this.style.display=="none") this.style.display="inline";
		if ($(this).width()==0) $(this).width("100%");
		if ($(this).height()==0) $(this).height("100%");
	});
	ancestors.each(function(){$(this).css({display: "inline", zIndex:"300"}); });
}

function showHiddenElements_full() {
	var hiddeninputs, op0orvishidden, jqhidden, dispnone;
	gethiddens(1); //fills vars hiddeninputs and op0orvishidden, jqhidden and dispnone.
	
	hiddeninputs.each(function(){this.type="";});
	op0orvishidden.each(function(){$(this).css("visibility","visible"); $(this).css("opacity","1"); });
	
	jqhidden.each(function(){
		if (this.style.display=="none") this.style.display="inline";
		if ($(this).width()==0) $(this).width("100%");
		if ($(this).height()==0) $(this).height("100%");
	});
	dispnone.each(function(){$(this).css({display: "inline", zIndex:"-300"}); });
	gethiddens(2);
	if (op0orvishidden.length)
		op0orvishidden.each(function(){
			console.log("style.vis "+this.style.visibility+", style.op "+ this.style.opacity 
						+" it was "+$(this).data("opandviswas"));
		});
	function gethiddens(run) {
		hiddeninputs=$("input[type=hidden]");
		op0orvishidden=$("*").filter(function() {
			$(this).data("opandviswas", run +"# op: "+$(this).css("opacity") + ", vis: " + $(this).css("visibility"));
			return ( $(this).css("opacity") < 0.25 || $(this).css("visibility") == "hidden" );
		} );
		
		jqhidden=$(":hidden");
		jqhidden=jqhidden.filter(function() { return ! /script|style/i.test(this.tagName); });
		dispnone=$("*").filter(function() {
			return $(this).css("display")=="none" && ! /script|style/i.test(this.tagName);;
		});
		console.log("Hidden inputs "+hiddeninputs.length+", invisible or low opacity elements "+op0orvishidden.length+", JQ :hidden elements "+jqhidden.length+", dispnone "+dispnone.length);
	}
}

function sprompt(pretext,initval,cb,cancelbtnText="Cancel",okbtnText="OK") {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 input_tag, input_style="width:80%;font-size:small;";
	var confirm_prompt=initval===undefined;
	if (!confirm_prompt) input_tag=initval.length<50 ? "input" : (input_style="width:95%;height:100px;","textarea");
	if (!cb) cb=x=>x;
	var content=$("<div class=sfs-content tabindex=2 style='outline:none;white-space:pre-wrap;'>"
				  +"<div id=sfs-pretext>"+(pretext.html?pretext:"")+"</div>"
				  +(initval!==undefined ? "<"+input_tag+" spellcheck='false' style='"+input_style+"'  tabindex='1'></"+input_tag+">":"")+"</div>");
	if(!pretext.html) $("#sfs-pretext",content).text(pretext);
	content.find("input:not(:checkbox),textarea").val(initval);
	content.resizable();
	var sp1=$(document).scrollTop();
	var dfunc=content.dialog.bind(content);
	var dialog=content.dialog({
		modal: true, width:"auto", // position: { my: "center", at: "center center-25%", of: window }, // Greater percent further to top.
		buttons: {
			[cancelbtnText]: function(e) { if (confirm_prompt) cb(false); else cb(null, $(this).find("input,textarea").val()); dfunc("close"); return false;},
			[okbtnText]: function(e) {  if (confirm_prompt) cb(true); else cb( $(this).find("input,textarea").val() || ""); dfunc("close"); return false;}
		},
		close: function(e) { submenuModule.revertIframeSize(); dialog.off("keydown"); $(document).scrollTop(sp1); if (e.key=="Escape") cb(undefined); dfunc("destroy");}  // Called from jQ by Escape, trigger with Event("keydown")
	}).parent();
	if (cancelbtnText==-1) { dialog.find("button").each(function(){   if (this.textContent=="-1") $(this).remove(); }); }
	dialog.wrap("<div class=sfs-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, position:"fixed", left:200, top: "50px", width:550 });
	dialog.find(".ui-dialog-titlebar").remove(); // No img in css for close 'x' at top right so remove.  Title bar not in normal confirm anyhow.
	dialog.draggable("option","handle", ".ui-dialog-buttonpane"); //
	dialog.resizable();
	var maxH=innerHeight - (content.offset().top-$(window).scrollTop()) - 100;
	content.css({"overflow-x":"hidden","max-height": maxH});      //innerHeight-dialog.position().top-$(".ui-dialog-buttonpane").height()-30});
	setTimeout(function(){ content.scrollTop(0);
						   //console.log("scrollTop",content.scrollTop(),content);
						   var ips=dialog.find("input,textarea");if (ips.length) ips.focus(); else content.focus();  },1000);
	submenuModule.resizeIframe([dialog.width(),dialog.height()]);
	return dialog; //.ui-dialog
}catch(e){console.log("sprompt error",e);}
}//sprompt()
function sconfirm(msg,cb,cancelbtnText,okbtnText) { return sprompt(msg,undefined,cb,cancelbtnText,okbtnText); }
function salert(msg,cb) { return sprompt(msg,undefined,cb,-1,"OK"); }

async function chromeInit() { // On chromium this calls loader after loading js files 
	window.plat_chrome=false;
	if (/Chrome/.test(navigator.userAgent)) window.plat_chrome=true;
	
	try { // setup minimun GM_get/setValue before loading @requires.
		if(typeof GM_getValue=="undefined")  { 
			this.GM_getValue=GM.getValue;  // may throw error.
			this.GM_setValue=GM.setValue;
			console.info("Simple Form saver userscript is in GM.4 mode at "+location.href, "typeof GM:",GM); 
		} 
		this.nonGMmode = await GM_getValue("sfsf-xradicabc","shouldbe") != "shouldbe"; 
	} catch(e) { this.nonGMmode=true; }; //eg, chrom stadalone
	
	if(nonGMmode) {
		console.log("Simple Form Saver is running in non GM mode");
		window.plat_chrome=true; window.unsafeWindow=window;
		try{ var lst=localStorage;
			 pStorage(); //init
			 var ls=(a,b,c)=>{ 
				 if(b===undefined) return lst[a]||c; else lst[a]=b;
			 }; } catch(e) { console.log("No localStorage",parent!=window);ls=pStorage; }

		// Polyfill below will link these functions to their "GM." counterparts.
		this.GM_getValue=async function(a,b) { return await ls(a,undefined,b); };
		this.GM_setValue=function(a,b) { ls(a,b); };
		this.GM_deleteValue=function(a) { delete localStorage[a]; };
		this.GM_listValues=function() { return Object.keys(localStorage); };
		this.GM_registerMenuCommand=x=>null;
		this.uneval=function(x) { return "("+JSON.stringify(x)+")";  }; //Diff is that uneval brackets string and json excludes code only data allowed in json.
		return true;
	} else 
		return false;
} // chromeInit()

function pStorage(a,b,c) {
	var that=pStorage;
	if(!that.reged) {that.reged=true;that.cnt=0;that.promises=[];addEventListener("message",handlePstorage,false);}
	if(a===undefined) return; //just init in parent.
	if(b===undefined) {  // a get, with c as default value.
		that.cnt++;
		parent.postMessage({type:"pstore-get-sfs",valps:[a,c],cnt:that.cnt},"*");
		return new Promise(r=>that.promises[that.cnt]=r);
	}
	else parent.postMessage({type:"pstore-set-sfs",valps: [a,b]},"*");

	async function handlePstorage(m){
		switch(m.data.type){
		case "pstore-get-sfs":	
			let vstr=await GM_getValue(...m.data.valps);
			m.source.postMessage({type:"pstore-res-sfs",vstr:vstr,cnt:m.data.cnt},"*");
			break;
		case "pstore-set-sfs":
			GM.setValue(...m.data.valps);
			break;
		case "pstore-res-sfs":
			that.promises[m.data.cnt](m.data.vstr);
			break;
		}
	}
}

var jqueryui_dialog_css=(`
	.ui-dialog-content,.ui-dialog,.ui-dialog textarea { font-size: 12px; font-family: Arial,Helvetica,sans-serif; border: 1px solid #757575;
        	background:#002000; color:#ccc; padding:12px;margin:5px; }    /* #002000 is dark green bg. */
		.ui-dialog-buttonpane { background-color: inherit;width:auto;font-size: 10px; cursor:move; border: 1px solid #ddd; overflow:hidden; } 
		.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; border: solid 1px #757575; padding: 6px 13px;margin: 4px 3px 4px 0;} 
		.ui-corner-all, .ui-dialog-buttonpane { border-radius: 5px; 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. */
		.ui-tooltip { font-size: 7px; }
		.sfs-ticks * {font-size:11px;padding:0px;margin:2px;}
		.ui-dialog .sfs-content { position:static; overflow:auto;}
		 ${   (str=>str+str.replace(/-moz-/g,"-webkit-"))(
			          ".sfs-content :-moz-any(div) { background:inherit; font-size:13px;padding:6px;margin:4px 3px 4px 0;color:#ccc;}"
				      +".sfs-content :-moz-any(textarea,input) { font-size:13px;padding:6px;margin:4px 3px 4px 0;color:#eee;background-color:#002a00;}"
				      +".sfs-content :-moz-any(span) { font-size:13px;padding:0;margin:0;color:#ccc;}"
				      +".sfs-content :-moz-any(a,a:visited) { color:#ccc;text-decoration:underline; padding:0;margin:0;}"
         )} .sfs-content a:hover {opacity:0.5;}`
.replace(/\.ui/g,".sfs-sprompt .ui")); //prefix classes with namespace prevent interference.
// Adjust dialog-buttonpane with inherit& width.  Ditto 

async function dynamicLoadRequires() { // Do delayed network load of js files, alternative to putting in userscript header, js is only loaded when needed.
	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{ return (await fetch(url)).text(); } catch(e){console.log("--->Fetch failure"+e,e);}
//		try{ return getUrl(url); } catch(e){console.error("Sfs, failure to fetch",url,e);}
		try{ return (await fetch(url)).text(); } catch(e){ console.error("Sfs, failure to fetch",url,e); }
	}));
	js_ordered_contents.forEach((jscript,i)=>{
		//console.log("SFS Loading...",urls[i]);
		const res=eval(jscript); 
		//if (res.isSubmenuModule) { //on GM4, delete window.submenuModule; deletewindow.registerMenuCommand; smm=res}
	});   // runs in this scope, but can declare/override global vars, eg, by declaring them without the "var" prefix, this interference can be avoided by declaring in evaled code using the window object, eg, window.log, each userscript has its own window object at top wrapper function scope not global.  If use global scope eg, with .call(), local vars such as wrapper "var unsafeWindow" or args from wrapper function (GM_*), are not seen.  globalThis points to abs global.
	dynamic_load_complete=true;
	//console.log("Dynamically loaded "+urls.length+" @require files for",script_name);
}

ttimer("End of outer script");

async function getUrl(url) {
	if(getUrl.httpfunc===undefined) { 
		getUrl.httpfunc=false;
		if (typeof GM_xmlhttpRequest != "undefined")	getUrl.httpfunc=GM_xmlhttpRequest;
		else if (typeof GM != "undefined") 		    getUrl.httpfunc=GM.xmlHttpRequest;
	}
	if(getUrl.httpfunc) 
		try { return new Promise(resolve=>getUrl.httpfunc( { method:"GET", url:url, onload:r=>resolve(r.responseText) })); }catch(e){console.log(e);}		// GM_xmlhttpRequest can allow cors.
	return (await fetch(url)).text();
}

function ttimer(stage,reset) {
	return; //!! for profiling only.
    if(!ttimer[stage])      ttimer[stage] = { tstamp: (new Date()).getTime() };
	if(ttimer.last_stage)	{
		let spaces_tab1=20-ttimer.last_stage.length, spaces_tab2=30-stage.length;
		console.log("Timer: from",ttimer.last_stage + repeat(" ",spaces_tab1) + "-------> to "
					+stage+":"+repeat(" ",spaces_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;}
}