[Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids (now for v2!)

Post your working scripts, libraries and tools for AHK v1.1 and older
burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 24 Jun 2021, 13:08

@erohtar, maybe this will help. This is an example scarcely changed from those available from the GitHub page. Look for 'Neutron.ahk-1.0.0\Examples\Simple\'. I put the code below in the same folder.

Basically, since Neutron creates a COM instance of the Trident control, you can use those functions. Here I callNeutron.wnd.execScript("TestMe()") in the code for Button #1. See line 19 of 'Simple2.html'.

If you look at the Neutron.ahk code you'll see 'Neutron.wnd' is equivalent, if you consider 'wb' to be a COM object browser control, to wb.Document.parentWindow, so you can call "Neutron.wnd.execScript(<your function here>)

Simple2.html:
Spoiler
The AHK code is also scarcely changed.

Code: Select all

Example1_Button(neutron, event)
{
	; event.target will contain the HTML Element that fired the event.
	; Show a message box with its inner text.
	Neutron.wnd.execScript("TestMe()")
	MsgBox, % "You clicked: " event.target.innerText
}
Full listing of Simple2.ahk is below:
Spoiler
When you run Simple2.ahk and click on Button #1 you should see this alert, followed by the original AHK msgbox.
alert.PNG
alert.PNG (37.28 KiB) Viewed 5830 times
Also, this COM tutorial link may help.


Regards,
burque505

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 24 Jun 2021, 13:41

@burque505
YESS!! That's exactly what I needed - many thanks mate!
I'm looking up more information on that execScript method and anything else I can find around it.

This just opens up whole new channel of communication between AHK and Neutron window.
Below ahk code (with zero changes to html) is something that's really cool to me right now lol

Code: Select all

;outsource some activity to JS and get output right back to AHK
neutron.wnd.execScript("ahk.receive(eval(25*35))")

;display output received from JS
receive(neutron, result) {
	msgbox %result%
}

burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 24 Jun 2021, 14:46

@erohtar, glad it worked for you. Thanks for rekindling my interest in Neutron.ahk. Nice little code snippet, by the way! :thumbup:
Regards,
burque505

User avatar
oldbrother
Posts: 265
Joined: 23 Oct 2013, 05:08

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by oldbrother » 05 Jul 2021, 07:47

Great job!

It seems controls like button, edit can be easily done with HTML, but what should we do with ListView's? I feel it's not easily to replace a ListView with HTML Form. (I'm not an expert on either AHK or HTML/Javascript).
Last edited by oldbrother on 06 Jul 2021, 08:46, edited 1 time in total.

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by kczx3 » 06 Jul 2021, 07:28

@oldbrother Agreed, it isn't easy to replace the ListView. And that's only if you consider the Report style option of a ListView. There are many javascript libraries out there for dealing with tables that would be able to mimic most, if not all, of the functionality of a Report style ListView though.

Tre4shunter
Posts: 139
Joined: 26 Jan 2016, 16:05

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by Tre4shunter » 06 Jul 2021, 10:28

I've found 'Tabulator' to be a great library, which I use all the time in normal browser development, and also with neutron, and Chrome.ahk.

check it out!

http://tabulator.info/

Thanks,

-Tre4

User avatar
oldbrother
Posts: 265
Joined: 23 Oct 2013, 05:08

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by oldbrother » 06 Jul 2021, 19:31

Thank you all!

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 07 Jul 2021, 13:37

Given the fact that future of IE/Trident isn't bright, I've been looking into somehow mimicking same/similar functionality using Chrome. I'm no expert like him, but here's GeekDude's Template running in Chrome (using chrome.ahk):
Image

I simply ran Chrome in app mode (removes address bar, tabs, buttons etc), and removed the built-in title bar from GeekDude's Template html.

Chrome.ahk already supports getting and setting values from/to page elements, running javascript code etc, so that's a very solid platform to build more features on so that it can mimic Neutron even closer. This also means we can use latest BootStrap which has dropped support for IE completely.
Any thoughts/interest?

User avatar
joedf
Posts: 8937
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by joedf » 07 Jul 2021, 14:46

Neat work! :+1:
Maybe share your fork on github?
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 07 Jul 2021, 15:35

@joedf Oh it's not github worthy yet, but it's a start towards Chrome based GUIs, if it takes off.
Here is my AHK for launching that GUI

Code: Select all

Global chromeInst1,pageInst1

chromeExe=E:\Portables2\Chrome\App\Chrome-bin\chrome.exe
chromeProfile=E:\Portables2\Chrome\Data\profile

chromeW := 640
chromeH := 700
chromeX := Round((A_ScreenWidth - chromeW) / 2, 0)
chromeY := Round((A_ScreenHeight - chromeH) / 2, 0)

;attach to running instance or start new
if (Chromes := Chrome.FindInstances())
  chromeInst1 := {"base": Chrome, "DebugPort": Chromes.MinIndex()}
else
  chromeInst1 := Chm_Launch(chromeExe,chromeProfile, debugMode, 1, " --force-device-scale-factor --new-window --safe-mode --window-size=""" chromeW "," chromeH """ --window-position=""" chromeX "," chromeY """ --app=file://R:\Dropbox\Projects\test\neutron.html")

pageInst1 := chromeInst1.GetPage()

; Set up a timer to demonstrate making dynamic page updates every so often.
SetTimer, DynamicContent, 100
return

esc::
	chm_exit(chromeInst1,pageInst1)
ExitApp


DynamicContent() {
	; Get the mouse position
	MouseGetPos, x, y
	
	; Update the page with the new position
	chm_Set(pageInst1,"Id","ahk_x",1,"i",x)
	chm_Set(pageInst1,"Id","ahk_y",1,"i",y)
}

#Include R:\Dropbox\Projects\+Includes\f_chrome.ahk
btw the dynamic content part works, but I haven't added any functions to the buttons so far. This is still just a proof of concept.

Here's a function that I added to chrome.ahk, just to make things a bit easier for me with the most common options I use

Code: Select all

chm_launch(chromeExe:="", chromeProfile:="", chromeVisible:=0, chromeIncognito:=0, chromeParams:="") {
	If (chromeExe <> "")
	IfNotExist, %chromeExe%
	{
		MsgBox, 16, %appName% ERROR, Chrome.exe not found at given path:`n%chromeExe%
		ExitApp
	}
	
	if chromeVisible = 0
		chromeFlags .= " --headless"
	if chromeIncognito = 1
		chromeFlags .= " --incognito"
	
	chromeFlags .= " --no-default-browser-check " chromeParams

	chromeInst := new Chrome(chromeProfile, "about:blank", chromeFlags, chromeExe, "")
	
	If chromeVisible = 1
	IfNotInString, chromeParams, --app=		;if running in app mode, then starting url is already provided other than about:blank
	{
		WinWait, about:blank - Google Chrome,, 10
		Sleep, 200
	}
	Return chromeInst
}
And finally, here's the modified HTML of GeekDude's template (basically just removed the titlebar, as Chrome creates exactly the same title bar (with the addition of site/favicon)

Code: Select all

<!DOCTYPE html><html>
<head>

  <meta http-equiv="X-UA-Compatible" content="IE=edge">

  <link href="bootstrap.min.css" rel="stylesheet">
  <script src="jquery.min.js"></script>
  <script src="bootstrap.min.js"></script>
<style>
	html, body {
		width: 100%; height: 100%;
		margin: 0; padding: 0;
		font-family: sans-serif;
	}

	body {
		display: flex;
		flex-direction: column;
	}

	header {
		width: 100%;
		display: flex;
		background: silver;
		font-family: Segoe UI;
		font-size: 9pt;
	}

	.title-bar {
		padding: 0.35em 0.5em;
		flex-grow: 1;
	}

	.title-btn {
		padding: 0.35em 1.0em;
		cursor: pointer;
		vertical-align: bottom;
		font-family: Webdings;
		font-size: 11pt;
	}

	.title-btn:hover {
		background: rgba(0, 0, 0, .2);
	}

	.title-btn-close:hover {
		background: #dc3545;
	}

	.main {
		flex-grow: 1;
		padding: 0.5em;
		overflow: auto;
	}
</style>
<style>/* Make the title bar dark with light text */
	header {
		background: #333;
		color: white;
	}

	/* Make the content area dark with light text */
	.main {
		background: #444;
		color: white;
	}

	/* Make input boxes follow the dark theme */
	input {
		margin: 0.25em;
		padding: 0.5em;
		border: none;
		background: #333;
		color: white;
		border-radius: 0.25em;
	}
	:-ms-input-placeholder {
		color: silver;
	}
	button {
		background: slategray;
		border: none;
		color: white;
		padding: 0.25em 0.5em;
		border-radius: 0.25em;
	}</style>

</head>
<title>Neutron App</title>
<body>

<header>
</header>

<div class='main'><h1>Welcome to Neutron!</h1>

	<p>
		Neutron provides a powerful set of tools for build HTML-based user interfaces
		with AutoHotkey. It leverages the Trident engine, known for its use in Internet
		Explorer, because of its deep integration with the Microsoft Windows operating
		system and its wide availability across systems.
	</p>
	<p>
		This example is designed to show how to use the default Neutron template page.
		Because it uses the default template, it is also the simplest example to use
		and tweak as a beginner.
	</p>
	<p>
		It is also designed to show how you would apply your own theming to the
		template without having to modify it directly, by applying CSS styling to
		the built-in template title bar elements.
	</p>

	<h2>Example Button</h2>
	<button onclick="ahk.Clicked(event)">Click Me!</button>

	<h2>Example Form</h2>
	<form onsubmit="ahk.Submitted(event)">
		<label for="firstName">First Name:</label>
		<input type="text" id="firstName" placeholder="John" required>
		<br>
		<label for="lastName">Last Name:</label>
		<input type="text" id="lastName" placeholder="Smith" required>
		<br>
		<button type="submit">Submit</button>
	</form>

	<h2>Example Dynamic Content</h2>
	<p>
		Your mouse is at <span id="ahk_x">0</span>, <span id="ahk_y">0</span>.
	</p></div>

<script>// Write some JavaScript here</script>

</body>
</html>

User avatar
joedf
Posts: 8937
Joined: 29 Sep 2013, 17:08
Location: Canada
Contact:

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by joedf » 07 Jul 2021, 16:26

No worries, thanks for sharing! :+1:
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500, NVIDIA GTX 1060 6GB, 2x16GB Kingston FURY Beast - DDR4 3200 MHz | [About Me] | [About the AHK Foundation] | [Courses on AutoHotkey]
[ASPDM - StdLib Distribution] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library]

burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 08 Jul 2021, 11:14

@erohtar, this looks really cool. Might I ask if you're able to share the code of
#Include R:\Dropbox\Projects\+Includes\f_chrome.ahk
?

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 08 Jul 2021, 11:25

@burque505
Yes sure! Though it's just GeekDude's Chrome.ahk, with a few functions I created or enhanced for my use.
My additions are at the top of the code.

Code: Select all

chm_launch(chromeExe:="", chromeProfile:="", chromeVisible:=0, chromeIncognito:=0, chromeParams:="") {
	If (chromeExe <> "")
	IfNotExist, %chromeExe%
	{
		MsgBox, 16, %appName% ERROR, Chrome.exe not found at given path:`n%chromeExe%
		ExitApp
	}
	
	if chromeVisible = 0
		chromeFlags .= " --headless"
	if chromeIncognito = 1
		chromeFlags .= " --incognito"
	
	chromeFlags .= " --no-default-browser-check " chromeParams

	chromeInst := new Chrome(chromeProfile, "about:blank", chromeFlags, chromeExe, "")
	
	If chromeVisible = 1
	IfNotInString, chromeParams, --app=		;if running in app mode, then starting url is already provided other than about:blank
	{
		WinWait, about:blank - Google Chrome,, 10
		Sleep, 200
	}
	Return chromeInst
}

chm_exit(chromeInst,Tab) {
	Tab.close()
	;~ Tab.Call("Browser.close")
	Tab.Disconnect()
	chromeInst.Kill()
}


chm_scrollBy(Tab, x, y) {
	Tab.evaluate("window.scrollBy(" x "," y ")")
}

chm_scrollIntoView(Tab, method, identifier, index:=1) {
	If method = selector
		Try c := Tab.Evaluate("document.querySelector('" identifier "').scrollIntoView()")
	Else If method = class
		Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].scrollIntoView()")
	Else If method = tag
		Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].scrollIntoView()")
	Else If method = id
		Try c := Tab.Evaluate("document.getElementById('" identifier "').scrollIntoView()")
	Else If method = name
		Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].scrollIntoView()")
}

chm_click(Tab, method, identifier, index:=1) {
	If method = selector
		Try c := Tab.Evaluate("document.querySelector('" identifier "').click()")
	Else If method = class
		Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].click()")
	Else If method = tag
		Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].click()")
	Else If method = id
		Try c := Tab.Evaluate("document.getElementById('" identifier "').click()")
	Else If method = name
		Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].click()")
}


chm_waitForElement(Tab, method, identifier, index:=1) {
	Loop
	{
		If method = selector
			Try c := Tab.Evaluate("document.querySelector('" identifier "')")
		Else If method = class
			Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "]")
		Else If method = tag
			Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "]")
		Else If method = id
			Try c := Tab.Evaluate("document.getElementById('" identifier "')")
		Else If method = name
			Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "]")
		
		;~ If debugMode <> 0
			;~ msgbox %c%
		
		If (IsObject(c) = 1)
			break
		
		Sleep, 100
	}
}

chm_waitForElementContent(content, Tab, method, identifier, index:=1, valType := "value" ) {
	Loop
	{
		If method = selector
			Try c := Tab.Evaluate("document.querySelector('" identifier "')." valType).value
		Else If method = class
			Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "]." valType).value
		Else If method = tag
			Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "]." valType).value
		Else If method = id
			Try c := Tab.Evaluate("document.getElementById('" identifier "')." valType).value
		Else If method = name
			Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "]." valType).Value
		
		;~ If debugMode <> 0
			;~ msgbox %c%
		
		IfInString, c, %content%
			break
		
		Sleep, 100
	}
}

chm_table2array(Tab, method, identifier, index:=1) {
	
	If method = selector
		Try e := Tab.Evaluate("document.querySelector('" identifier "').outerHTML")
	Else If method = class
		Try e := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].outerHTML")
	Else If method = tag
		Try e := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].outerHTML")
	Else If method = id
		Try e := Tab.Evaluate("document.getElementById('" identifier "').outerHTML")
	Else If method = name
		Try e := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].outerHTML")

	d := ComObjCreate("HTMLFile")
	d.write(e.value)

	tObj := []
	loop % d.getElementsByTagName("table")[0].rows.length
	{
		row:= (A_Index-1)
		loop % d.getElementsByTagName("table")[0].rows[row].cells.length
		{
			col := (A_Index-1)
			tObj[row, col] := d.getElementsByTagName("table")[0].rows[row].cells[col].innertext
		}
	}
	d := ""
	return tObj
}

;********************Method: Class, Tag, Name, ID  **** ***********************************
;********************Get Property***********************************
chm_Get(Tab,Method="Class",Attribute="hfeed",Index=1){
 Obj:={}
 ;***********Class*******************
 if (Format("{:L}",Method)="class"){ ;Case-insensitive check to see if method = Class
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].textContent").Value
 } ;***********Tag*******************
 Else if (Format("{:L}",Method)="tag"){  ;Case-insensitive check to see if method = Tag
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].textContent").Value
 } ;************Name******************
 Else if (Format("{:L}",Method)="name"){  ;Case-insensitive check to see if method = Name
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].textContent").Value
 } ;************Selector******************
 Else if (Format("{:L}",Method)="selector"){  ;Case-insensitive check to see if method = Selector
 Obj.OuterHTML:= Tab.Evaluate("document.querySelector('" Attribute "').outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.querySelector('" Attribute "').value").Value
 Obj.InnerText:= Tab.Evaluate("document.querySelector('" Attribute "').innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.querySelector('" Attribute "').textContent").Value
 } ;***********ID*******************
 Else if (Format("{:L}",Method)="id"){  ;Case-insensitive check to see if method = ID
 Obj.OuterHTML:= Tab.Evaluate("document.getElementById('" Attribute "').outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementById('" Attribute "').value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementById('" Attribute "').innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementById('" Attribute "').textContent").Value
 } Else{ MsgBox fix Method - Valid values are: Name, Class, Tag, Selector, ID
}
return obj
}

;~ chm_Set(Tab,Method:="Class",Attribute:="s",Index:=2,Property:="v",Value:="Just a test")
;~ chm_Set(Tab,Method:="Name",Attribute:="s",Index:=2,Property:="v",Value:="Just another test")
;~ chm_Set(Tab,Method:="tag",Attribute:="Input",Index:=1,Property:="v",Value:="Just a test")
;~ chm_Set(Tab,Method:="ID",Attribute:="cat",Index:=2,Property:="v",Value:="11")
 

chm_Set(Tab,Method="Class",Attribute="hfeed",Index=1,Property="i",Value=""){
 ;***********Class*******************
 if (Format("{:L}",Method)="class"){  ;Case-insensitive check to see if method = Class
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Tag*******************
 Else if (Format("{:L}",Method)="tag"){ ;Case-insensitive check to see if method = Tag
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Name*******************
 Else if (Format("{:L}",Method)="Name"){ ;Case-insensitive check to see if method = Name
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Selector*******************
 Else if (Format("{:L}",Method)="Selector"){ ;Case-insensitive check to see if method = Selector
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.querySelector('" Attribute "').outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.querySelector('" Attribute "').value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.querySelector('" Attribute "').innerText='" Value "'").Value
 } ;***********ID*******************
 Else if (Format("{:L}",Method)="id"){ ;Case-insensitive check to see if method = ID
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementById('" Attribute "').outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementById('" Attribute "').value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementById('" Attribute "').innerText='" Value "'").Value
 } Else{ MsgBox fix Method- Valid values are: Name, Class, Tag, Selector, ID (case of text does not matter)
}
}

; Chrome.ahk v1.2
; Copyright GeekDude 2018
; https://github.com/G33kDude/Chrome.ahk

class Chrome
{
	static DebugPort := 9222
	
	/*
		Escape a string in a manner suitable for command line parameters
	*/
	CliEscape(Param)
	{
		return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
	}
	
	/*
		Finds instances of chrome in debug mode and the ports they're running
		on. If no instances are found, returns a false value. If one or more
		instances are found, returns an associative array where the keys are
		the ports, and the values are the full command line texts used to start
		the processes.
		
		One example of how this may be used would be to open chrome on a
		different port if an instance of chrome is already open on the port
		you wanted to used.
		
		```
		; If the wanted port is taken, use the largest taken port plus one
		DebugPort := 9222
		if (Chromes := Chrome.FindInstances()).HasKey(DebugPort)
			DebugPort := Chromes.MaxIndex() + 1
		ChromeInst := new Chrome(ProfilePath,,,, DebugPort)
		```
		
		Another use would be to scan for running instances and attach to one
		instead of starting a new instance.
		
		```
		if (Chromes := Chrome.FindInstances())
			ChromeInst := {"base": Chrome, "DebugPort": Chromes.MinIndex()}
		else
			ChromeInst := new Chrome(ProfilePath)
		```
	*/
	FindInstances()
	{
		static Needle := "--remote-debugging-port=(\d+)"
		Out := {}
		for Item in ComObjGet("winmgmts:")
			.ExecQuery("SELECT CommandLine FROM Win32_Process"
			. " WHERE Name = 'chrome.exe'")
			if RegExMatch(Item.CommandLine, Needle, Match)
				Out[Match1] := Item.CommandLine
		return Out.MaxIndex() ? Out : False
	}
	
	/*
		ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
		URLs        - The page or array of pages for Chrome to load when it opens
		Flags       - Additional flags for chrome when launching
		ChromePath  - Path to chrome.exe, will detect from start menu when left blank
		DebugPort   - What port should Chrome's remote debugging server run on
	*/
	__New(ProfilePath:="", URLs:="about:blank", Flags:="", ChromePath:="", DebugPort:="")
	{
		; Verify ProfilePath
		if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
			throw Exception("The given ProfilePath does not exist")
		this.ProfilePath := ProfilePath
		
		; Verify ChromePath
		if (ChromePath == "")
			FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
		if (ChromePath == "")
			RegRead, ChromePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe
		if !FileExist(ChromePath)
			throw Exception("Chrome could not be found")
		this.ChromePath := ChromePath
		
		; Verify DebugPort
		if (DebugPort != "")
		{
			if DebugPort is not integer
				throw Exception("DebugPort must be a positive integer")
			else if (DebugPort <= 0)
				throw Exception("DebugPort must be a positive integer")
			this.DebugPort := DebugPort
		}
		
		; Escape the URL(s)
		for Index, URL in IsObject(URLs) ? URLs : [URLs]
			URLString .= " " this.CliEscape(URL)
		
		Run, % this.CliEscape(ChromePath)
		. " --remote-debugging-port=" this.DebugPort
		. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
		. (Flags ? " " Flags : "")
		. URLString
		,,, OutputVarPID
		this.PID := OutputVarPID
	}
	
	/*
		End Chrome by terminating the process.
	*/
	Kill()
	{
		Process, Close, % this.PID
	}
	
	/*
		Queries chrome for a list of pages that expose a debug interface.
		In addition to standard tabs, these include pages such as extension
		configuration pages.
	*/
	GetPageList()
	{
		http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		http.open("GET", "http://127.0.0.1:" this.DebugPort "/json")
		http.send()
		return this.Jxon_Load(http.responseText)
	}
	
	/*
		Returns a connection to the debug interface of a page that matches the
		provided criteria. When multiple pages match the criteria, they appear
		ordered by how recently the pages were opened.
		
		Key        - The key from the page list to search for, such as "url" or "title"
		Value      - The value to search for in the provided key
		MatchMode  - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
		Index      - If multiple pages match the given criteria, which one of them to return
		fnCallback - A function to be called whenever message is received from the page
	*/
	GetPageBy(Key, Value, MatchMode:="exact", Index:=1, fnCallback:="")
	{
		Count := 0
		for n, PageData in this.GetPageList()
		{
			if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
				|| (MatchMode = "contains" && InStr(PageData[Key], Value))
				|| (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
				|| (MatchMode = "regex" && PageData[Key] ~= Value))
				&& ++Count == Index)
				return new this.Page(PageData.webSocketDebuggerUrl, fnCallback)
		}
	}
	
	/*
		Shorthand for GetPageBy("url", Value, "startswith")
	*/
	GetPageByURL(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
	{
		return this.GetPageBy("url", Value, MatchMode, Index, fnCallback)
	}
	
	/*
		Shorthand for GetPageBy("title", Value, "startswith")
	*/
	GetPageByTitle(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
	{
		return this.GetPageBy("title", Value, MatchMode, Index, fnCallback)
	}
	
	/*
		Shorthand for GetPageBy("type", Type, "exact")
		
		The default type to search for is "page", which is the visible area of
		a normal Chrome tab.
	*/
	GetPage(Index:=1, Type:="page", fnCallback:="")
	{
		return this.GetPageBy("type", Type, "exact", Index, fnCallback)
	}
	
	/*
		Connects to the debug interface of a page given its WebSocket URL.
	*/
	class Page
	{
		Connected := False
		ID := 0
		Responses := []
		
		/*
			wsurl      - The desired page's WebSocket URL
			fnCallback - A function to be called whenever message is received
		*/
		__New(wsurl, fnCallback:="")
		{
			this.fnCallback := fnCallback
			this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
			
			; TODO: Throw exception on invalid objects
			if IsObject(wsurl)
				wsurl := wsurl.webSocketDebuggerUrl
			
			wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
			this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
			this.ws.__New(wsurl)
			
			while !this.Connected
				Sleep, 50
		}
		
		/*
			Calls the specified endpoint and provides it with the given
			parameters.
			
			DomainAndMethod - The endpoint domain and method name for the
			endpoint you would like to call. For example:
			PageInst.Call("Browser.close")
			PageInst.Call("Schema.getDomains")
			
			Params - An associative array of parameters to be provided to the
			endpoint. For example:
			PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
			, "landscape": Chrome.Jxon_True() ; Boolean Value
			, "pageRanges: "1-5, 8, 11-13"}) ; String value
			PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
			
			WaitForResponse - Whether to block until a response is received from
			Chrome, which is necessary to receive a return value, or whether
			to continue on with the script without waiting for a response.
		*/
		Call(DomainAndMethod, Params:="", WaitForResponse:=True)
		{
			if !this.Connected
				throw Exception("Not connected to tab")
			
			; Use a temporary variable for ID in case more calls are made
			; before we receive a response.
			ID := this.ID += 1
			this.ws.Send(Chrome.Jxon_Dump({"id": ID
			, "params": Params ? Params : {}
			, "method": DomainAndMethod}))
			
			if !WaitForResponse
				return
			
			; Wait for the response
			this.responses[ID] := False
			while !this.responses[ID]
				Sleep, 50
			
			; Get the response, check if it's an error
			response := this.responses.Delete(ID)
			if (response.error)
				throw Exception("Chrome indicated error in response",, Chrome.Jxon_Dump(response.error))
			
			return response.result
		}
		
		/*
			Run some JavaScript on the page. For example:
			
			PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
			PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
		*/
		Evaluate(JS)
		{
			response := this.Call("Runtime.evaluate",
			( LTrim Join
			{
				"expression": JS,
				"objectGroup": "console",
				"includeCommandLineAPI": Chrome.Jxon_True(),
				"silent": Chrome.Jxon_False(),
				"returnByValue": Chrome.Jxon_False(),
				"userGesture": Chrome.Jxon_True(),
				"awaitPromise": Chrome.Jxon_False()
			}
			))
			
			if (response.exceptionDetails)
				throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails))
			
			return response.result
		}
		
		/*
			Waits for the page's readyState to match the DesiredState.
			
			DesiredState - The state to wait for the page's ReadyState to match
			Interval     - How often it should check whether the state matches
		*/
		WaitForLoad(DesiredState:="complete", Interval:=100)
		{
			while this.Evaluate("document.readyState").value != DesiredState
				Sleep, %Interval%
		}
		
		/*
			Internal function triggered when the script receives a message on
			the WebSocket connected to the page.
		*/
		Event(EventName, Event)
		{
			; If it was called from the WebSocket adjust the class context
			if this.Parent
				this := this.Parent
			
			; TODO: Handle Error events
			if (EventName == "Open")
			{
				this.Connected := True
				BoundKeepAlive := this.BoundKeepAlive
				SetTimer, %BoundKeepAlive%, 15000
			}
			else if (EventName == "Message")
			{
				data := Chrome.Jxon_Load(Event.data)
				
				; Run the callback routine
				fnCallback := this.fnCallback
				if (newData := %fnCallback%(data))
					data := newData
				
				if this.responses.HasKey(data.ID)
					this.responses[data.ID] := data
			}
			else if (EventName == "Close")
			{
				this.Disconnect()
			}
			else if (EventName == "Error")
			{
				throw Exception("Websocket Error!")
			}
		}
		
		/*
			Disconnect from the page's debug interface, allowing the instance
			to be garbage collected.
			
			This method should always be called when you are finished with a
			page or else your script will leak memory.
		*/
		Disconnect()
		{
			if !this.Connected
				return
			
			this.Connected := False
			this.ws.Delete("Parent")
			this.ws.Disconnect()
			
			BoundKeepAlive := this.BoundKeepAlive
			SetTimer, %BoundKeepAlive%, Delete
			this.Delete("BoundKeepAlive")
		}
		
		class WebSocket
		{
			__New(WS_URL)
			{
				static wb
				
				; Create an IE instance
				Gui, +hWndhOld
				Gui, New, +hWndhWnd
				this.hWnd := hWnd
				Gui, Add, ActiveX, vWB, Shell.Explorer
				Gui, %hOld%: Default
				
				; Write an appropriate document
				WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
				. "content='IE=edge'><body></body>")
				while (WB.ReadyState < 4)
					sleep, 50
				this.document := WB.document
				
				; Add our handlers to the JavaScript namespace
				this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
				this.document.parentWindow.ahk_event := this._Event.Bind(this)
				this.document.parentWindow.ahk_ws_url := WS_URL
				
				; Add some JavaScript to the page to open a socket
				Script := this.document.createElement("script")
				Script.text := "ws = new WebSocket(ahk_ws_url);`n"
				. "ws.onopen = function(event){ ahk_event('Open', event); };`n"
				. "ws.onclose = function(event){ ahk_event('Close', event); };`n"
				. "ws.onerror = function(event){ ahk_event('Error', event); };`n"
				. "ws.onmessage = function(event){ ahk_event('Message', event); };"
				this.document.body.appendChild(Script)
			}
			
			; Called by the JS in response to WS events
			_Event(EventName, Event)
			{
				this["On" EventName](Event)
			}
			
			; Sends data through the WebSocket
			Send(Data)
			{
				this.document.parentWindow.ws.send(Data)
			}
			
			; Closes the WebSocket connection
			Close(Code:=1000, Reason:="")
			{
				this.document.parentWindow.ws.close(Code, Reason)
			}
			
			; Closes and deletes the WebSocket, removing
			; references so the class can be garbage collected
			Disconnect()
			{
				if this.hWnd
				{
					this.Close()
					Gui, % this.hWnd ": Destroy"
					this.hWnd := False
				}
			}
		}
	}
	
	Jxon_Load(ByRef src, args*)
	{
		static q := Chr(34)
		
		key := "", is_key := false
		stack := [ tree := [] ]
		is_arr := { (tree): 1 }
		next := q . "{[01234567890-tfn"
		pos := 0
		while ( (ch := SubStr(src, ++pos, 1)) != "" )
		{
			if InStr(" `t`n`r", ch)
				continue
			if !InStr(next, ch, true)
			{
				ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
				col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
				
				msg := Format("{}: line {} col {} (char {})"
				,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
				: (next == "'")     ? "Unterminated string starting at"
				: (next == "\")     ? "Invalid \escape"
				: (next == ":")     ? "Expecting ':' delimiter"
				: (next == q)       ? "Expecting object key enclosed in double quotes"
				: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
				: (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
				: (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
				: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
				, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
				, ln, col, pos)
				
				throw Exception(msg, -1, ch)
			}
			
			is_array := is_arr[obj := stack[1]]
			
			if i := InStr("{[", ch)
			{
				val := (proto := args[i]) ? new proto : {}
				is_array? ObjPush(obj, val) : obj[key] := val
				ObjInsertAt(stack, 1, val)
				
				is_arr[val] := !(is_key := ch == "{")
				next := q . (is_key ? "}" : "{[]0123456789-tfn")
			}
			
			else if InStr("}]", ch)
			{
				ObjRemoveAt(stack, 1)
				next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
			}
			
			else if InStr(",:", ch)
			{
				is_key := (!is_array && ch == ",")
				next := is_key ? q : q . "{[0123456789-tfn"
			}
			
			else ; string | number | true | false | null
			{
				if (ch == q) ; string
				{
					i := pos
					while i := InStr(src, q,, i+1)
					{
						val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
						static end := A_AhkVersion<"2" ? 0 : -1
						if (SubStr(val, end) != "\")
							break
					}
					if !i ? (pos--, next := "'") : 0
						continue
					
					pos := i ; update pos
					
					val := StrReplace(val,    "\/",  "/")
					, val := StrReplace(val, "\" . q,    q)
					, val := StrReplace(val,    "\b", "`b")
					, val := StrReplace(val,    "\f", "`f")
					, val := StrReplace(val,    "\n", "`n")
					, val := StrReplace(val,    "\r", "`r")
					, val := StrReplace(val,    "\t", "`t")
					
					i := 0
					while i := InStr(val, "\",, i+1)
					{
						if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
							continue 2
						
						; \uXXXX - JSON unicode escape sequence
						xxxx := Abs("0x" . SubStr(val, i+2, 4))
						if (A_IsUnicode || xxxx < 0x100)
							val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
					}
					
					if is_key
					{
						key := val, next := ":"
						continue
					}
				}
				
				else ; number | true | false | null
				{
					val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
					
					; For numerical values, numerify integers and keep floats as is.
					; I'm not yet sure if I should numerify floats in v2.0-a ...
					static number := "number", integer := "integer"
					if val is %number%
					{
						if val is %integer%
							val += 0
					}
					; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
					; SOMETIMES return strings due to certain optimizations. Since it
					; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
					else if (val == "true" || val == "false")
						val := %value% + 0
					; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
					; as it would raise an exception in AHK_H(overriding built-in var)
					else if (val == "null")
						val := ""
					; any other values are invalid, continue to trigger error
					else if (pos--, next := "#")
						continue
					
					pos += i-1
				}
				
				is_array? ObjPush(obj, val) : obj[key] := val
				next := obj==tree ? "" : is_array ? ",]" : ",}"
			}
		}
		
		return tree[1]
	}
	
	Jxon_Dump(obj, indent:="", lvl:=1)
	{
		static q := Chr(34)
		
		if IsObject(obj)
		{
			static Type := Func("Type")
			if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
				throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
			
			prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0))
			fn_t := prefix "Jxon_True",  obj_t := this ? %fn_t%(this) : %fn_t%()
			fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%()
			
			if (&obj == &obj_t)
				return "true"
			else if (&obj == &obj_f)
				return "false"
			
			is_array := 0
			for k in obj
				is_array := k == A_Index
			until !is_array
			
			static integer := "integer"
			if indent is %integer%
			{
				if (indent < 0)
					throw Exception("Indent parameter must be a postive integer.", -1, indent)
				spaces := indent, indent := ""
				Loop % spaces
					indent .= " "
			}
			indt := ""
			Loop, % indent ? lvl : 0
				indt .= indent
			
			this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc
			lvl += 1, out := "" ; Make #Warn happy
			for k, v in obj
			{
				if IsObject(k) || (k == "")
					throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
				
				if !is_array
					out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key
				.  ( indent ? ": " : ":" ) ; token + padding
				out .= %this_fn%(v, indent, lvl) ; value
				.  ( indent ? ",`n" . indt : "," ) ; token + indent
			}
			
			if (out != "")
			{
				out := Trim(out, ",`n" . indent)
				if (indent != "")
					out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
			}
			
			return is_array ? "[" . out . "]" : "{" . out . "}"
		}
		
		; Number
		else if (ObjGetCapacity([obj], 1) == "")
			return obj
		
		; String (null -> not supported by AHK)
		if (obj != "")
		{
			obj := StrReplace(obj,  "\",    "\\")
			, obj := StrReplace(obj,  "/",    "\/")
			, obj := StrReplace(obj,    q, "\" . q)
			, obj := StrReplace(obj, "`b",    "\b")
			, obj := StrReplace(obj, "`f",    "\f")
			, obj := StrReplace(obj, "`n",    "\n")
			, obj := StrReplace(obj, "`r",    "\r")
			, obj := StrReplace(obj, "`t",    "\t")
			
			static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
			while RegExMatch(obj, needle, m)
				obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
		}
		
		return q . obj . q
	}
	
	Jxon_True()
	{
		static obj := {}
		return obj
	}
	
	Jxon_False()
	{
		static obj := {}
		return obj
	}
}

burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 08 Jul 2021, 11:37

Thank you, @erohtar! Would you mind testing this? I added your functions to the top of @teadrinker's version, which I've been calling tdChrome.ahk.

Code: Select all

chm_launch(chromeExe:="", chromeProfile:="", chromeVisible:=0, chromeIncognito:=0, chromeParams:="") {
	If (chromeExe <> "")
	IfNotExist, %chromeExe%
	{
		MsgBox, 16, %appName% ERROR, Chrome.exe not found at given path:`n%chromeExe%
		ExitApp
	}
	
	if chromeVisible = 0
		chromeFlags .= " --headless"
	if chromeIncognito = 1
		chromeFlags .= " --incognito"
	
	chromeFlags .= " --no-default-browser-check " chromeParams

	chromeInst := new Chrome(chromeProfile, "about:blank", chromeFlags, chromeExe, "")
	
	If chromeVisible = 1
	IfNotInString, chromeParams, --app=		;if running in app mode, then starting url is already provided other than about:blank
	{
		WinWait, about:blank - Google Chrome,, 10
		Sleep, 200
	}
	Return chromeInst
}

chm_exit(chromeInst,Tab) {
	Tab.close()
	;~ Tab.Call("Browser.close")
	Tab.Disconnect()
	chromeInst.Kill()
}


chm_scrollBy(Tab, x, y) {
	Tab.evaluate("window.scrollBy(" x "," y ")")
}

chm_scrollIntoView(Tab, method, identifier, index:=1) {
	If method = selector
		Try c := Tab.Evaluate("document.querySelector('" identifier "').scrollIntoView()")
	Else If method = class
		Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].scrollIntoView()")
	Else If method = tag
		Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].scrollIntoView()")
	Else If method = id
		Try c := Tab.Evaluate("document.getElementById('" identifier "').scrollIntoView()")
	Else If method = name
		Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].scrollIntoView()")
}

chm_click(Tab, method, identifier, index:=1) {
	If method = selector
		Try c := Tab.Evaluate("document.querySelector('" identifier "').click()")
	Else If method = class
		Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].click()")
	Else If method = tag
		Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].click()")
	Else If method = id
		Try c := Tab.Evaluate("document.getElementById('" identifier "').click()")
	Else If method = name
		Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].click()")
}


chm_waitForElement(Tab, method, identifier, index:=1) {
	Loop
	{
		If method = selector
			Try c := Tab.Evaluate("document.querySelector('" identifier "')")
		Else If method = class
			Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "]")
		Else If method = tag
			Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "]")
		Else If method = id
			Try c := Tab.Evaluate("document.getElementById('" identifier "')")
		Else If method = name
			Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "]")
		
		;~ If debugMode <> 0
			;~ msgbox %c%
		
		If (IsObject(c) = 1)
			break
		
		Sleep, 100
	}
}

chm_waitForElementContent(content, Tab, method, identifier, index:=1, valType := "value" ) {
	Loop
	{
		If method = selector
			Try c := Tab.Evaluate("document.querySelector('" identifier "')." valType).value
		Else If method = class
			Try c := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "]." valType).value
		Else If method = tag
			Try c := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "]." valType).value
		Else If method = id
			Try c := Tab.Evaluate("document.getElementById('" identifier "')." valType).value
		Else If method = name
			Try c := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "]." valType).Value
		
		;~ If debugMode <> 0
			;~ msgbox %c%
		
		IfInString, c, %content%
			break
		
		Sleep, 100
	}
}

chm_table2array(Tab, method, identifier, index:=1) {
	
	If method = selector
		Try e := Tab.Evaluate("document.querySelector('" identifier "').outerHTML")
	Else If method = class
		Try e := Tab.Evaluate("document.getElementsByClassName('" identifier "')[" index-1 "].outerHTML")
	Else If method = tag
		Try e := Tab.Evaluate("document.getElementsByTagName('" identifier "')[" index-1 "].outerHTML")
	Else If method = id
		Try e := Tab.Evaluate("document.getElementById('" identifier "').outerHTML")
	Else If method = name
		Try e := Tab.Evaluate("document.getElementsByName('" identifier "')[" Index-1 "].outerHTML")

	d := ComObjCreate("HTMLFile")
	d.write(e.value)

	tObj := []
	loop % d.getElementsByTagName("table")[0].rows.length
	{
		row:= (A_Index-1)
		loop % d.getElementsByTagName("table")[0].rows[row].cells.length
		{
			col := (A_Index-1)
			tObj[row, col] := d.getElementsByTagName("table")[0].rows[row].cells[col].innertext
		}
	}
	d := ""
	return tObj
}

;********************Method: Class, Tag, Name, ID  **** ***********************************
;********************Get Property***********************************
chm_Get(Tab,Method="Class",Attribute="hfeed",Index=1){
 Obj:={}
 ;***********Class*******************
 if (Format("{:L}",Method)="class"){ ;Case-insensitive check to see if method = Class
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].textContent").Value
 } ;***********Tag*******************
 Else if (Format("{:L}",Method)="tag"){  ;Case-insensitive check to see if method = Tag
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].textContent").Value
 } ;************Name******************
 Else if (Format("{:L}",Method)="name"){  ;Case-insensitive check to see if method = Name
 Obj.OuterHTML:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].textContent").Value
 } ;************Selector******************
 Else if (Format("{:L}",Method)="selector"){  ;Case-insensitive check to see if method = Selector
 Obj.OuterHTML:= Tab.Evaluate("document.querySelector('" Attribute "').outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.querySelector('" Attribute "').value").Value
 Obj.InnerText:= Tab.Evaluate("document.querySelector('" Attribute "').innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.querySelector('" Attribute "').textContent").Value
 } ;***********ID*******************
 Else if (Format("{:L}",Method)="id"){  ;Case-insensitive check to see if method = ID
 Obj.OuterHTML:= Tab.Evaluate("document.getElementById('" Attribute "').outerHTML").Value
 Obj.Value:= Tab.Evaluate("document.getElementById('" Attribute "').value").Value
 Obj.InnerText:= Tab.Evaluate("document.getElementById('" Attribute "').innerText").Value
 Obj.TextContent:= Tab.Evaluate("document.getElementById('" Attribute "').textContent").Value
 } Else{ MsgBox fix Method - Valid values are: Name, Class, Tag, Selector, ID
}
return obj
}

;~ chm_Set(Tab,Method:="Class",Attribute:="s",Index:=2,Property:="v",Value:="Just a test")
;~ chm_Set(Tab,Method:="Name",Attribute:="s",Index:=2,Property:="v",Value:="Just another test")
;~ chm_Set(Tab,Method:="tag",Attribute:="Input",Index:=1,Property:="v",Value:="Just a test")
;~ chm_Set(Tab,Method:="ID",Attribute:="cat",Index:=2,Property:="v",Value:="11")
 

chm_Set(Tab,Method="Class",Attribute="hfeed",Index=1,Property="i",Value=""){
 ;***********Class*******************
 if (Format("{:L}",Method)="class"){  ;Case-insensitive check to see if method = Class
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByClassName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Tag*******************
 Else if (Format("{:L}",Method)="tag"){ ;Case-insensitive check to see if method = Tag
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByTagName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Name*******************
 Else if (Format("{:L}",Method)="Name"){ ;Case-insensitive check to see if method = Name
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementsByName('" Attribute "')[" Index-1 "].innerText='" Value "'").Value
 } ;***********Selector*******************
 Else if (Format("{:L}",Method)="Selector"){ ;Case-insensitive check to see if method = Selector
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.querySelector('" Attribute "').outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.querySelector('" Attribute "').value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.querySelector('" Attribute "').innerText='" Value "'").Value
 } ;***********ID*******************
 Else if (Format("{:L}",Method)="id"){ ;Case-insensitive check to see if method = ID
 if (Format("{:L}",Property)="o") ;If Property="o" then set OuterHTML
 Tab.Evaluate("document.getElementById('" Attribute "').outerHTML='" Value "'").Value
 Else if (Format("{:L}",Property)="v") ;If Property="v" then set Value
 Tab.Evaluate("document.getElementById('" Attribute "').value='" Value "'").Value
 Else if (Format("{:L}",Property)="i") ;If Property="i" then set innerText
 Tab.Evaluate("document.getElementById('" Attribute "').innerText='" Value "'").Value
 } Else{ MsgBox fix Method- Valid values are: Name, Class, Tag, Selector, ID (case of text does not matter)
}
}


class Chrome
{
   static DebugPort := 9222
   
   /*
      Escape a string in a manner suitable for command line parameters
   */
   CliEscape(Param)
   {
      return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
   }
   
   /*
      Finds instances of chrome in debug mode and the ports they're running
      on. If no instances are found, returns a false value. If one or more
      instances are found, returns an associative array where the keys are
      the ports, and the values are the full command line texts used to start
      the processes.
      
      One example of how this may be used would be to open chrome on a
      different port if an instance of chrome is already open on the port
      you wanted to used.
      
      ```
      ; If the wanted port is taken, use the largest taken port plus one
      DebugPort := 9222
      if (Chromes := Chrome.FindInstances()).HasKey(DebugPort)
         DebugPort := Chromes.MaxIndex() + 1
      ChromeInst := new Chrome(ProfilePath,,,, DebugPort)
      ```
      
      Another use would be to scan for running instances and attach to one
      instead of starting a new instance.
      
      ```
      if (Chromes := Chrome.FindInstances())
         ChromeInst := {"base": Chrome, "DebugPort": Chromes.MinIndex(), PID: Chromes[Chromes.MinIndex(), "PID"]}
      else
         ChromeInst := new Chrome(ProfilePath)
      ```
   */
   FindInstances()
   {
      Out := {}
      for Item in ComObjGet("winmgmts:").ExecQuery("SELECT * FROM Win32_Process WHERE Name = 'chrome.exe'")
         if RegExMatch(Item.CommandLine, "i)chrome.exe""?\s+--remote-debugging-port=(\d+)", Match)
            Out[Match1] := {cmd: Item.CommandLine, PID: Item.ProcessId}
      return Out.MaxIndex() ? Out : False
   }
   
   /*
      ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
      URLs        - The page or array of pages for Chrome to load when it opens
      Flags       - Additional flags for chrome when launching
      ChromePath  - Path to chrome.exe, will detect from start menu when left blank
      DebugPort   - What port should Chrome's remote debugging server run on
   */
   __New(ProfilePath:="", URLs:="about:blank", Flags:="", ChromePath:="", DebugPort:="")
   {
      ; Verify ProfilePath
      if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
         throw Exception("The given ProfilePath does not exist")
      this.ProfilePath := ProfilePath
      
      ; Verify ChromePath
      if (ChromePath == "")
         FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
      if (ChromePath == "")
         RegRead, ChromePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe
      if !FileExist(ChromePath)
         throw Exception("Chrome could not be found")
      this.ChromePath := ChromePath
      
      ; Verify DebugPort
      if (DebugPort != "")
      {
         if DebugPort is not integer
            throw Exception("DebugPort must be a positive integer")
         else if (DebugPort <= 0)
            throw Exception("DebugPort must be a positive integer")
         this.DebugPort := DebugPort
      }
      
      ; Escape the URL(s)
      for Index, URL in IsObject(URLs) ? URLs : [URLs]
         URLString .= " " this.CliEscape(URL)
      
      Run, % this.CliEscape(ChromePath)
      . " --remote-debugging-port=" this.DebugPort
      . (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
      . (Flags ? " " Flags : "")
      . URLString
      ,,, OutputVarPID
      this.PID := OutputVarPID
   }
   
   /*
      End Chrome by terminating the process.
   */
   Kill()
   {
      Process, Close, % this.PID
   }
   
   /*
      Queries chrome for a list of pages that expose a debug interface.
      In addition to standard tabs, these include pages such as extension
      configuration pages.
   */
   GetPageList()
   {
      http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
      http.open("GET", "http://127.0.0.1:" this.DebugPort "/json")
      http.send()
      return LightJson.Parse(http.responseText)
   }
   
   /*
      Returns a connection to the debug interface of a page that matches the
      provided criteria. When multiple pages match the criteria, they appear
      ordered by how recently the pages were opened.
      
      Key        - The key from the page list to search for, such as "url" or "title"
      Value      - The value to search for in the provided key
      MatchMode  - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
      Index      - If multiple pages match the given criteria, which one of them to return
      fnCallback - A function to be called whenever message is received from the page
   */
   GetPageBy(Key, Value, MatchMode:="exact", Index:=1, fnCallback:="")
   {
      Count := 0
      for n, PageData in this.GetPageList()
      {
         if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
            || (MatchMode = "contains" && InStr(PageData[Key], Value))
            || (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
            || (MatchMode = "regex" && PageData[Key] ~= Value))
            && ++Count == Index)
            return new this.Page(PageData.webSocketDebuggerUrl, fnCallback)
      }
   }
   
   /*
      Shorthand for GetPageBy("url", Value, "startswith")
   */
   GetPageByURL(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
   {
      return this.GetPageBy("url", Value, MatchMode, Index, fnCallback)
   }
   
   /*
      Shorthand for GetPageBy("title", Value, "startswith")
   */
   GetPageByTitle(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
   {
      return this.GetPageBy("title", Value, MatchMode, Index, fnCallback)
   }
   
   /*
      Shorthand for GetPageBy("type", Type, "exact")
      
      The default type to search for is "page", which is the visible area of
      a normal Chrome tab.
   */
   GetPage(Index:=1, Type:="page", fnCallback:="")
   {
      return this.GetPageBy("type", Type, "exact", Index, fnCallback)
   }
   
   /*
      Connects to the debug interface of a page given its WebSocket URL.
   */
   class Page
   {
      Connected := False
      ID := 0
      Responses := []
      
      /*
         wsurl      - The desired page's WebSocket URL
         fnCallback - A function to be called whenever message is received
      */
      __New(wsurl, fnCallback:="")
      {
         this.fnCallback := fnCallback
         this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
         
         ; TODO: Throw exception on invalid objects
         if IsObject(wsurl)
            wsurl := wsurl.webSocketDebuggerUrl
         
         wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
         this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
         this.ws.__New(wsurl)
         
         while !this.Connected
            Sleep, 50
      }
      
      /*
         Calls the specified endpoint and provides it with the given
         parameters.
         
         DomainAndMethod - The endpoint domain and method name for the
         endpoint you would like to call. For example:
         PageInst.Call("Browser.close")
         PageInst.Call("Schema.getDomains")
         
         Params - An associative array of parameters to be provided to the
         endpoint. For example:
         PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
         , "landscape": LightJson.true ; Boolean Value
         , "pageRanges: "1-5, 8, 11-13"}) ; String value
         PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
         
         WaitForResponse - Whether to block until a response is received from
         Chrome, which is necessary to receive a return value, or whether
         to continue on with the script without waiting for a response.
      */
      Call(DomainAndMethod, Params:="", WaitForResponse:=True)
      {
         if !this.Connected
            throw Exception("Not connected to tab")
         
         ; Use a temporary variable for ID in case more calls are made
         ; before we receive a response.
         ID := this.ID += 1
         this.ws.Send(LightJson.Stringify({"id": ID
         , "params": Params ? Params : {}
         , "method": DomainAndMethod}))
         
         if !WaitForResponse
            return
         
         ; Wait for the response
         this.responses[ID] := False
         while !this.responses[ID]
            Sleep, 50
         
         ; Get the response, check if it's an error
         response := this.responses.Delete(ID)
         if (response.error)
            throw Exception("Chrome indicated error in response",, LightJson.Stringify(response.error))
         
         return response.result
      }
      
      /*
         Run some JavaScript on the page. For example:
         
         PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
         PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
      */
      Evaluate(JS)
      {
         response := this.Call("Runtime.evaluate",
         ( LTrim Join
         {
            "expression": JS,
            "objectGroup": "console",
            "includeCommandLineAPI": LightJson.true,
            "silent": LightJson.false,
            "returnByValue": LightJson.false,
            "userGesture": LightJson.true,
            "awaitPromise": LightJson.false
         }
         ))
         
         if (response.exceptionDetails)
            throw Exception(response.result.description,, LightJson.Stringify(response.exceptionDetails))
         
         return response.result
      }
      
      /*
         Waits for the page's readyState to match the DesiredState.
         
         DesiredState - The state to wait for the page's ReadyState to match
         Interval     - How often it should check whether the state matches
      */
      WaitForLoad(DesiredState:="complete", Interval:=100)
      {
         while this.Evaluate("document.readyState").value != DesiredState
            Sleep, Interval
      }
      
      /*
         Internal function triggered when the script receives a message on
         the WebSocket connected to the page.
      */
      Event(EventName, Event)
      {
         ; If it was called from the WebSocket adjust the class context
         if this.Parent
            this := this.Parent
         
         ; TODO: Handle Error events
         if (EventName == "Open")
         {
            this.Connected := True
            BoundKeepAlive := this.BoundKeepAlive
            SetTimer, %BoundKeepAlive%, 15000
         }
         else if (EventName == "Message")
         {
            data := LightJson.Parse(Event.data)
            
            ; Run the callback routine
            fnCallback := this.fnCallback
            if (newData := %fnCallback%(data))
               data := newData
            
            if this.responses.HasKey(data.ID)
               this.responses[data.ID] := data
         }
         else if (EventName == "Close")
         {
            this.Disconnect()
         }
         else if (EventName == "Error")
         {
            throw Exception("Websocket Error!")
         }
      }
      
      /*
         Disconnect from the page's debug interface, allowing the instance
         to be garbage collected.
         
         This method should always be called when you are finished with a
         page or else your script will leak memory.
      */
      Disconnect()
      {
         if !this.Connected
            return
         
         this.Connected := False
         this.ws.Delete("Parent")
         this.ws.Disconnect()
         
         BoundKeepAlive := this.BoundKeepAlive
         SetTimer, %BoundKeepAlive%, Delete
         this.Delete("BoundKeepAlive")
      }
      
      class WebSocket
      {
         __New(WS_URL)
         {
            static wb
            
            ; Create an IE instance
            Gui, +hWndhOld
            Gui, New, +hWndhWnd
            this.hWnd := hWnd
            Gui, Add, ActiveX, vWB, Shell.Explorer
            Gui, %hOld%: Default
            
            ; Write an appropriate document
            WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
            . "content='IE=edge'><body></body>")
            while (WB.ReadyState < 4)
               sleep, 50
            this.document := WB.document
            
            ; Add our handlers to the JavaScript namespace
            this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
            this.document.parentWindow.ahk_event := this._Event.Bind(this)
            this.document.parentWindow.ahk_ws_url := WS_URL
            
            ; Add some JavaScript to the page to open a socket
            Script := this.document.createElement("script")
            Script.text := "ws = new WebSocket(ahk_ws_url);`n"
            . "ws.onopen = function(event){ ahk_event('Open', event); };`n"
            . "ws.onclose = function(event){ ahk_event('Close', event); };`n"
            . "ws.onerror = function(event){ ahk_event('Error', event); };`n"
            . "ws.onmessage = function(event){ ahk_event('Message', event); };"
            this.document.body.appendChild(Script)
         }
         
         ; Called by the JS in response to WS events
         _Event(EventName, Event)
         {
            this["On" EventName](Event)
         }
         
         ; Sends data through the WebSocket
         Send(Data)
         {
            this.document.parentWindow.ws.send(Data)
         }
         
         ; Closes the WebSocket connection
         Close(Code:=1000, Reason:="")
         {
            this.document.parentWindow.ws.close(Code, Reason)
         }
         
         ; Closes and deletes the WebSocket, removing
         ; references so the class can be garbage collected
         Disconnect()
         {
            if this.hWnd
            {
               this.Close()
               Gui, % this.hWnd ": Destroy"
               this.hWnd := False
            }
         }
      }
   }
}

class LightJson
{
   static JS := LightJson.GetJS(), true := {}, false := {}, null := {}
   
   Parse(json, _rec := false) {
      if !_rec
         obj := this.Parse(this.JS.eval("(" . json . ")"), true)
      else if !IsObject(json)
         obj := json
      else if this.JS.Object.prototype.toString.call(json) == "[object Array]" {
         obj := []
         Loop % json.length
            obj.Push( this.Parse(json[A_Index - 1], true) )
      }
      else {
         obj := {}
         keys := this.JS.Object.keys(json)
         Loop % keys.length {
            k := keys[A_Index - 1]
            obj[k] := this.Parse(json[k], true)
         }
      }
      Return obj
   }
   
   Stringify(obj, indent := "") {
      if indent|1 {
         for k, v in ["true", "false", "null"]
            if (obj = this[v])
               Return v

         if IsObject( obj ) {
            isArray := true
            for key in obj {
               if IsObject(key)
                  throw Exception("Invalid key")
               if !( key = A_Index || isArray := false )
                  break
            }
            for k, v in obj
               str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : """" . k . """:" ) . this.Stringify(v, true)

            Return isArray ? "[" str "]" : "{" str "}"
         }
         else if !(obj*1 = "" || RegExMatch(obj, "\s"))
            Return obj
         
         for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(8), "\b"]]
            obj := StrReplace( obj, v[1], v[2] )

         Return """" obj """"
      }
      sObj := this.Stringify(obj, true)
      Return this.JS.eval("JSON.stringify(" . sObj . ",'','" . indent . "')")
   }
   
   GetJS() {
      static Doc, JS
      if !Doc {
         Doc := ComObjCreate("htmlfile")
         Doc.write("<meta http-equiv=""X-UA-Compatible"" content=""IE=9"">")
         JS := Doc.parentWindow
         ( Doc.documentMode < 9 && JS.execScript() )
      }
      Return JS
   }
}
EDIT: Here's a test I just ran with your version plus @teadrinker's chrome.ahk. There is some code at the end for my Win7 system where cmd.exe and conhost refuse to die.

Code: Select all

#Include f_chrome.ahk
SetTitleMatchMode, 2
FileCreateDir, ChromeProfile
SetBatchLines, -1

full_command_line := DllCall("GetCommandLine", "str")

if not (A_IsAdmin or RegExMatch(full_command_line, " /restart(?!\S)"))
{
    try
    {
        if A_IsCompiled
            Run *RunAs "%A_ScriptFullPath%" /restart
        else
            Run *RunAs "%A_AhkPath%" /restart "%A_ScriptFullPath%"
    }
    ExitApp
}


;****************
; Fix these paths for your system
; or the world will explode
; without notice
;****************

;FileCreateDir, ChromeProfile
ChromeInst := new Chrome("C:\Users\you\Desktop\AHK\Chrome.ahk\Chrome.ahk-1.2\ChromeProfile")
dlPath := "C:\ResearchDownloads\Testing"
If (FileExist("C:\ResearchDownloads\Testing\abc.txt"))
	FileDelete, C:\ResearchDownloads\Testing\abc.txt
webpage := "https://www.autohotkey.com"


Sleep, 3000
PageInstance := ChromeInst.GetPage()
PageInstance.WaitForLoad()
WinMaximize, ahk_exe chrome.exe
PageInstance.Call("Page.navigate", {"url": webpage})

;****************
; Make Chrome download to a specific directory
; @gregster mentions this at https://www.autohotkey.com/boards/viewtopic.php?f=76&t=77637&hilit=page.setdownloadbehavior
; I didn't see that till after I tried this, my syntax is marginally different (quotes around behavior and downloadPath)
; Works both ways.
;****************
PageInstance.Call("Page.setDownloadBehavior", {"behavior" : "allow", "downloadPath": dlPath})
PageInstance.WaitForLoad()

;****************
; This JS will grab the innerHTML of title at autohotkey.com, set the mime_type,
; create a blob of mime_type (i.e. text), create a hidden anchor, set the href
; of the anchor to the blob location, 
; grab the href of that blob, stringify it, 
; alert with the stringified href, download the blob as 'abc.txt' to the
; custom download directory in 'dlPath'.
;****************
js =
(
var myData = document.getElementById("MainTitle").innerHTML
var mime_type = "text/plain";
var blob = new Blob([myData], {type: mime_type});
let hidden = document.createElement('a');
hidden.href = window.URL.createObjectURL(blob);
var stringy = hidden.href.toString();
alert(stringy);
hidden.download = 'abc.txt';
hidden.click();
hidden.remove();
)
PageInstance.Evaluate(js)

;~ A search for 'window.URL.createObjectURL(blob)'
;~ may be useful to you.

Sleep, 5000
PageInstance.Call("Browser.close")
ChromeInst.Disconnect()
PageInstance.Disconnect()

; Overkill, no doubt. Works.
PageInstance := ""
ChromeInst := ""
ChromeInst.Kill()
WinKill, chrome.exe

; Kill conhost.exe so it doesn't hang around.
; 
DetectHiddenWindows, On
; From GeekDude's tips and tricks: https://www.autohotkey.com/boards/viewtopic.php?f=7&t=7190
Run, cmd,, Hide, PID
WinWait, ahk_pid %PID%
DllCall("AttachConsole", "UInt", PID)

; Run another process that would normally
; make a command prompt pop up, such as
; RunWait, cmd /c dir > PingOutput.txt
Runwait, taskkill /im conhost.exe /f 
; Thanks to @flyingDman post from 2010, 
; https://autohotkey.com/board/topic/49732-kill-process/

; Close the hidden command prompt process
Process, Close, %PID%
ExitApp

Edit of Edit: Could I prevail upon you to show some code using your functions in f_chrome.ahk? They look very useful.
Last edited by burque505 on 08 Jul 2021, 12:08, edited 1 time in total.

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 08 Jul 2021, 12:07

@burque505
Sorry I'm a little lost on what exactly are we testing?

But your script does everything it's supposed to though (I changed the paths to my chrome exe and profile), and then it ran, showed the alert, downloaded the text file, and exited cleanly.
And that little silent download js snippet looks useful :)

burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 08 Jul 2021, 12:37

@erohtar, sorry, just wanted to see if your f_chrome.ahk would work with @teadrinker's code for JSON instead of the original. It does.
Are your functions for use in your Neutron-type code? Duh ... :headwall:
Just loaded your example code, works great. Just made this little change.

Code: Select all

pageInst1 := chromeInst1.GetPage()
pageInst1.WaitForLoad()

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 08 Jul 2021, 13:22

@burque505
Oh okay, got it.
Those functions I added to chrome.ahk were just to make automating Chrome easier. They reduce the effort involved in getting/setting values based on different kind of identifiers, waiting for elements to show up or have a particular value, or reading a table into an array. Just things I came across and thought it'd make it easier next time if I add these.

I've only recently discovered Neutron, and trying to make it work on Chrome platform is something I've been playing with last few days. With @Tre4shunter's help, the first hurdle I was stumped with is cleared - I can have AHK react to some events generated by the page. I still don't have it totally figured out though - for example Neutron has built in support to handle Forms, which isn't working in NeutronC (lets call it that?) yet.

burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by burque505 » 08 Jul 2021, 13:50

@erohtar, NeutronC sounds good to me.

erohtar
Posts: 20
Joined: 24 Jun 2021, 10:53

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by erohtar » 12 Jul 2021, 13:32

@burque505
So I've come across the first major roadblock with NeutronC. When I run two GUIs simultaneously, the second GUI doesn't work properly. In the above example script, if I just make a copy of it and run both, only the first one responds to button clicks and has the dynamic content (mousePos) updated. This happens even if I run these two from separate profile folders.
And I don't think giving each GUI their own copy of portable Chrome is the best idea.

Tre4shunter
Posts: 139
Joined: 26 Jan 2016, 16:05

Re: [Library] Neutron.ahk - AutoHotkey Web GUIs on Steroids

Post by Tre4shunter » 12 Jul 2021, 20:52

@erohtar

Thats because you aren't incrementing the debug port probably. Each instance you run is looking at the same debugurl because they are all using port 9222(default)

There is a built in solution for getting/incrementing the port #, or handle it externally to make sure your instances dont overlap. Its in some of the examples i beleive - or just look into the 'FindInstances()' function within the Chrome class.

-Tre4

Post Reply

Return to “Scripts and Functions (v1)”