Javascript <---> GLua interface for HTML panels

So I found out that you can communicate back to Lua from HTML panels by changing the window title. This is not very pretty nor easy-to-use of course so I made a script to ‘hook’ functions to Javascript.
This should make it easier to make things like interfaces and menus in HTML, and pass changes / events via Javascript back to Lua.

There’s a few things I’m not too happy with right now:

  • I’m using RunString to ‘decode’ the data string passed back from Javascript to Lua. Hacky but simple. A better way would be to encode the string into glon but that’d mean I’d need to convert most of this into Javascript and I really can’t be arsed. Feel free to do it for me :stuck_out_tongue:
  • Not too sure I’m getting all the corner cases of values people could pass. Needs more extensive testing.

Anyway, the Lua script:
[lua]
/*

Script by Clavus

Free to use for anything

Adds functions to improve communication between
the Javascript in HTML panels en GLua

*/

js = {}

local callbacks = {}
_JSHackyTable = {}

local function pageTitleChanged( panel, data_str )

local delimiter = "%*%"; 
local data = string.Explode( delimiter, data_str, false )

//print("Received: "..data_str)

if (#data != 2) then
	// not the right format for processing
	return
end

local key = data[1]

if (!callbacks[key]) then
	MsgN("Unhandled javascript callback '"..key.."'!")
	return;
end

// SO FUCKING HACKY
RunString("_JSHackyTable = "..data[2]) 

// Better solution would be to make a glon.encode function
// in javascript, and use that to encode the data string,
// but I really can't be arsed.

callbacks[key](unpack(_JSHackyTable))

end

local function jsConvertTypeToString( val )

local t = type(val)

if ( t == "number" or t == "boolean" ) then
	return tostring(val)
elseif ( t == "string" ) then
	return "\""..val.."\""
elseif ( t == "table" ) then
	local str = ""
	local prefix = ""
	
	for k, v in pairs( val ) do
		local keystr = jsConvertTypeToString( k ) // should be numbers or strings
		local valuestr = jsConvertTypeToString( v )
		str = str..prefix..keystr..": "..valuestr
		prefix = ", "
	end		
	
	return "{ "..str.." }"
else
	return ""
end

end

/---------------------------------------------------------
Name: js.CreateCallback( panel, key, func )
Desc: Links a function which the Javascript inside the HTML panel
can trigger using the specified key.
Usage:
---------------------------------------------------------
/
function js.CreateCallback( panel, key, func )

if (type(panel) != "Panel" and panel.RunJavaScript) then
	Error("Panel parameter needs to be a HTML panel!")
end

if (type(key) != "string") then
	Error("Callback key needs to be string!")
end

if (type(func) != "function") then
	Error("Callback function is not a function!")
end

if (panel.PageTitleChanged != pageTitleChanged) then
	panel.PageTitleChanged = pageTitleChanged
end

callbacks[key] = func

end

/---------------------------------------------------------
Name: js.RunJavascriptFunction( panel, func_name, … )
Desc: Run a javascript function in the specified HTML panel
with any amount of parameters. Beware that only parameters
of the type table, number, boolean and string are converted.
Other types will be considered nil and passed to Javascript
as “null”.
Usage:
---------------------------------------------------------
/
function js.RunJavascriptFunction( panel, func_name, … )

if (type(panel) != "Panel" and panel.RunJavaScript) then
	Error("Panel parameter needs to be a HTML panel!")
end

if (type(func_name) != "string") then
	Error("Javascript function name needs to be string!")
end

local str = ""
local prefix = ""

if (arg) then
	for k, v in pairs( arg ) do
		
		if (v == nil) then continue end
		
		local res = jsConvertTypeToString( v )
		if (res == "") then continue end
		
		str = str..prefix..res
		prefix = ", "
	
	end
end

local run_str = func_name.."( "..str.." );"

//MsgN("Running javascript: "..run_str)

panel:RunJavascript( run_str )

end
[/lua]

and the Javascript:


/*

Script by Clavus
http://steamcommunity.com/id/clavuselite
Free to use for anything

Adds functions to improve communication between
the Javascript in HTML panels en GLua

*/

function gLuaConvert( val )
{
	var t = typeof( val );
	
	if (t == "number" || t == "boolean") {
		return String(val);
	}
	else if (t == "string") {
		return "\"" + val + "\"";
	}
	else if (t == "object") {
		// if it's an object we just assume it's an associative array
		var str = "";
		var prefix = "";
		
		for( i in val ) {
		
			var keystr = gLuaConvert( i ); // should be numbers or strings
			var valuestr = gLuaConvert( val* );
			str += prefix + "[" + keystr + "] = " + valuestr;
			prefix = ", ";
		
		}
		
		return "{ " + str + " }";	
	}
	else {
		return "nil";
	}
	
}

/*
    Name: gLua.CallFuncion( key, ... )
    Desc: Call the Lua function connected with the specified
	      key with any amount of parameters. Only parameters
		  of the type number, string, boolean and Array objects
		  are converted properly. Others are considered 'undefined'
		  and passed to Lua as 'nil'.
   Usage:
*/

function gLuaCall( key )
{
	// using a preset argument delimiter is a bit crude but simple.
	// make sure it's the same in the lua script.
	var delimiter = "%*%"; 
	var str = key + delimiter + "{ ";
	var prefix = "";
	
	for( var i = 1; i < arguments.length; i++ ) {
		var val = arguments*;
		str += prefix + gLuaConvert( val );
		prefix = ", ";
	}
	
	str += " }";
	
	// title character limit seems to be 5000 chars
	if (str.length >= 5000) { 
		alert("Cannot call function '" + key + "', data string is too long (>5000 chars)! Try to spread the data over several calls."); 
	}
	
	document.title = str;

}

How to use

When we write this in Lua:
[lua]function RunHTMLTest()

HTMLTest = vgui.Create("HTML")

// In this case I have the above Javascript in garrysmod/data/glua_interface/glua_interface.txt
// On a webserver you could just put it in a file called glua_interface.js or something and include that in your page.
HTMLTest:SetHTML("<script>"..file.Read("glua_interface/glua_interface.txt").."</script>")

// Create a function hook with key 'a_callback', that prints all the values that are passed to it
js.CreateCallback( HTMLTest, "a_callback", function(...) print("a callback!") PrintTable(arg) end )

// couldn't call it in the same frame as I set the HTML for some reason (page errored that it couldn't find TestCallback)... oh well
timer.Simple(0,function()
	// run the TestCallback function in the HTML panel Javascript with a number, string and table parameter
	js.RunJavascriptFunction( HTMLTest, "TestCallback", 20.35, "Woof", { [1] = "number 1", [2] = true, ["toast"] = "not pork" } )
end)

end[/lua]

And put this in the Javascript on the HTML panel page:


function TestCallback(a, b, c)
{
	// do something with a b c
	a += 10;
	b += " said the dog";
	c["pork"] = "not toast";
	
	// call the Lua function with the key 'a_callback' and pass a, b, c as parameters
	gLuaCall('a_callback', a, b, c);
}

Calling RunHTMLTest() will print this in console:



a callback!
1	=	30.35
2	=	Woof said the dog
3:
		1	=	number 1
		pork	=	not toast
		toast	=	not pork
		2	=	true
n	=	3


Let me know what you think and how it could be improved.

This is… awesome. Thanks for this.

[lua]RunString("_JSHackyTable = "…data[2])[/lua] (line 37)
Why RunString? Wouldn’t just placing

[lua]_JSHackyTable = data[2][/lua]
do it?

EDIT: Actually, it should be a bit better performance wise since it makes less calls to Lua C functions.

Reminds me of the Lua editor I included in GMod a few updates ago

Because then _JSHackyTable would contain an unparsed string. Using RunString and assigning it to global variable is a quick way of letting the string be interpreted as Lua code.

Oh right, didn’t notice that you said it in the first post. My bad.

How exactly does that editor work ?
OT: Looks cool and love the “JSHackyTable” xD

How does it work?

It boils down to just a textbox with Lua syntax highlighting. Use RunString to run the code on the fly.

Nice work Clavus!

Today I started to code something similar using the query string (e.g. index.html?foo=bar), but I see you got much further than I did! I tried to include your LUA code, and embedded the JavaScript into a HTML Panel but I ended up with this error:



a callback!
[@lua\includes\util.lua:35] bad argument #1 to 'pairs' (table expected, got nil)


It seems that the line 124



	for k, v in pairs( arg ) do


is causing the error, because I don’t see where “arg” comes from?

It would be great garry, if you could make a backend to the HTML Panel which allows interaction with LUA - this would make it possible to create a variety of GUIs like the Toybox…

Ah nice find. It’s because you didn’t pass any additional parameters to the function ( the “…” represents an unlimited amount of additional parameters, passed to the function as the table ‘arg’ ). This should be possible of course so I updated the code in the OP with a simple check.

This works for me, thanks for the quick update!

Nonetheless I’m still interested in how the Toybox solves this - I tried to analyze the HTML source code and found some interesting JavaScript functions (from http://toybox.garrysmod.com/client/ingame.js?2):



function SendLua( lua )
{
	window.location = "lua://client/" + lua;
}

function StartSpawnmenuFocus( lua )
{
	window.location = "gmod://startspawnmenufocus/";
}

function EndSpawnmenuFocus( lua )
{
	window.location = "gmod://endspawnmenufocus/";
}


but they seem to be a dead end, I don’t find them anywhere else… I even analyzed each packet of the HTTP traffic quickly, but didn’t find something on the first look…

Lets just say there is a lot more :downs: