diff --git a/contributing/development/compiling/compiling_for_web.rst b/contributing/development/compiling/compiling_for_web.rst index baa9e6e1517..b33bfda15b1 100644 --- a/contributing/development/compiling/compiling_for_web.rst +++ b/contributing/development/compiling/compiling_for_web.rst @@ -43,7 +43,7 @@ either ``template_release`` for a release build or ``template_debug`` for a debu scons platform=web target=template_release scons platform=web target=template_debug -By default, the :ref:`JavaScript singleton ` will be built +By default, the :ref:`JavaScriptBridge singleton ` will be built into the engine. Official export templates also have the JavaScript singleton enabled. Since ``eval()`` calls can be a security concern, the ``javascript_eval`` option can be used to build without the singleton:: diff --git a/tutorials/export/exporting_for_web.rst b/tutorials/export/exporting_for_web.rst index b4b96599cf4..e5619c8abd0 100644 --- a/tutorials/export/exporting_for_web.rst +++ b/tutorials/export/exporting_for_web.rst @@ -284,109 +284,10 @@ supported on your web server for further file size savings. be used. Instead, you should use an established web server such as Apache or nginx. -.. _doc_javascript_eval: - -Calling JavaScript from script ------------------------------- - -In web builds, the ``JavaScriptBridge`` singleton is implemented. It offers a single -method called ``eval`` that works similarly to the JavaScript function of the -same name. It takes a string as an argument and executes it as JavaScript code. -This allows interacting with the browser in ways not possible with script -languages integrated into Godot. - -.. tabs:: - .. code-tab:: gdscript - - func my_func(): - JavaScriptBridge.eval("alert('Calling JavaScript per GDScript!');") - - .. code-tab:: csharp - - private void MyFunc() - { - JavaScriptBridge.Eval("alert('Calling JavaScript per C#!');") - } - -The value of the last JavaScript statement is converted to a GDScript value and -returned by ``eval()`` under certain circumstances: - - * JavaScript ``number`` is returned as :ref:`class_float` - * JavaScript ``boolean`` is returned as :ref:`class_bool` - * JavaScript ``string`` is returned as :ref:`class_String` - * JavaScript ``ArrayBuffer``, ``TypedArray`` and ``DataView`` are returned as :ref:`PackedByteArray` - -.. tabs:: - .. code-tab:: gdscript - - func my_func2(): - var js_return = JavaScriptBridge.eval("var myNumber = 1; myNumber + 2;") - print(js_return) # prints '3.0' - - .. code-tab:: csharp - - private void MyFunc2() - { - var jsReturn = JavaScriptBridge.Eval("var myNumber = 1; myNumber + 2;"); - GD.Print(jsReturn); // prints '3.0' - } - -Any other JavaScript value is returned as ``null``. - -HTML5 export templates may be :ref:`built ` without -support for the singleton to improve security. With such templates, and on -platforms other than HTML5, calling ``JavaScriptBridge.eval`` will also return -``null``. The availability of the singleton can be checked with the -``web`` :ref:`feature tag `: - -.. tabs:: - .. code-tab:: gdscript - - func my_func3(): - if OS.has_feature('web'): - JavaScriptBridge.eval(""" - console.log('The JavaScriptBridge singleton is available') - """) - else: - print("The JavaScriptBridge singleton is NOT available") - - .. code-tab:: csharp - - private void MyFunc3() - { - if (OS.HasFeature("web")) - { - JavaScriptBridge.Eval("console.log('The JavaScriptBridge singleton is available')"); - } - else - { - GD.Print("The JavaScriptBridge singleton is NOT available"); - } - } - -.. tip:: GDScript's multi-line strings, surrounded by 3 quotes ``"""`` as in - ``my_func3()`` above, are useful to keep JavaScript code readable. - -The ``eval`` method also accepts a second, optional Boolean argument, which -specifies whether to execute the code in the global execution context, -defaulting to ``false`` to prevent polluting the global namespace: - -.. tabs:: - .. code-tab:: gdscript - - func my_func4(): - # execute in global execution context, - # thus adding a new JavaScript global variable `SomeGlobal` - JavaScriptBridge.eval("var SomeGlobal = {};", true) - - .. code-tab:: csharp - - private void MyFunc4() - { - // execute in global execution context, - // thus adding a new JavaScript global variable `SomeGlobal` - JavaScriptBridge.Eval("var SomeGlobal = {};", true); - } +Interacting with the browser and JavaScript +------------------------------------------- + +See the :ref:`dedicated page ` on how to interact with JavaScript and access some unique Web browser features. Environment variables --------------------- diff --git a/tutorials/export/feature_tags.rst b/tutorials/export/feature_tags.rst index 4ccc5246f0b..b0183252315 100644 --- a/tutorials/export/feature_tags.rst +++ b/tutorials/export/feature_tags.rst @@ -132,7 +132,7 @@ Here is a list of most feature tags in Godot. Keep in mind they are **case-sensi exported to HTML5 on a mobile device. To check whether a project exported to HTML5 is running on a mobile device, - :ref:`call JavaScript code ` that reads the browser's + :ref:`call JavaScript code ` that reads the browser's user agent. Custom features diff --git a/tutorials/platform/web/index.rst b/tutorials/platform/web/index.rst index 6bb103473e1..85da0be7c7a 100644 --- a/tutorials/platform/web/index.rst +++ b/tutorials/platform/web/index.rst @@ -1,14 +1,15 @@ :allow_comments: False :article_outdated: True -.. _doc_platform_html5: +.. _doc_platform_web: -HTML5 -===== +Web +=== .. toctree:: :maxdepth: 1 :name: toc-learn-features-platform-html5 + javascript_bridge html5_shell_classref customizing_html5_shell diff --git a/tutorials/platform/web/javascript_bridge.rst b/tutorials/platform/web/javascript_bridge.rst new file mode 100644 index 00000000000..a3baf9c282f --- /dev/null +++ b/tutorials/platform/web/javascript_bridge.rst @@ -0,0 +1,325 @@ +.. _doc_web_javascript_bridge: + +The JavaScriptBridge Singleton +============================== + +In web builds, the :ref:`JavaScriptBridge ` singleton +allows interaction with JavaScript and web browsers, and can be used to implement some +functionalities unique to the web platform. + +Interacting with JavaScript +--------------------------- + +Sometimes, when exporting Godot for the Web, it might be necessary to interface +with external JavaScript code like third-party SDKs, libraries, or +simply to access browser features that are not directly exposed by Godot. + +The ``JavaScriptBridge`` singleton provides methods to wrap a native JavaScript object into +a Godot :ref:`JavaScriptObject ` that tries to feel +natural in the context of Godot scripting (e.g. GDScript and C#). + +The :ref:`JavaScriptBridge.get_interface() ` +method retrieves an object in the global scope. + +.. code-block:: gdscript + + extends Node + + func _ready(): + # Retrieve the `window.console` object. + var console = JavaScriptBridge.get_interface("console") + # Call the `window.console.log()` method. + console.log("test") + +The :ref:`JavaScriptBridge.create_object() ` +creates a new object via the JavaScript ``new`` constructor. + +.. code-block:: gdscript + + extends Node + + func _ready(): + # Call the JavaScript `new` operator on the `window.Array` object. + # Passing 10 as argument to the constructor: + # JS: `new Array(10);` + var arr = JavaScriptBridge.create_object("Array", 10) + # Set the first element of the JavaScript array to the number 42. + arr[0] = 42 + # Call the `pop` function on the JavaScript array. + arr.pop() + # Print the value of the `length` property of the array (9 after the pop). + print(arr.length) + +As you can see, by wrapping JavaScript objects into ``JavaScriptObject`` you can +interact with them like they were native Godot objects, calling their methods, +and retrieving (or even setting) their properties. + +Base types (int, floats, strings, booleans) are automatically converted (floats +might lose precision when converted from Godot to JavaScript). Anything else +(i.e. objects, arrays, functions) are seen as ``JavaScriptObjects`` themselves. + +Callbacks +--------- + +Calling JavaScript code from Godot is nice, but sometimes you need to call a +Godot function from JavaScript instead. + +This case is a bit more complicated. JavaScript relies on garbage collection, +while Godot uses reference counting for memory management. This means you have +to explicitly create callbacks (which are returned as ``JavaScriptObjects`` +themselves) and you have to keep their reference. + +Arguments passed by JavaScript to the callback will be passed as a single Godot +``Array``. + +.. code-block:: gdscript + + extends Node + + # Here we create a reference to the `_my_callback` function (below). + # This reference will be kept until the node is freed. + var _callback_ref = JavaScriptBridge.create_callback(_my_callback) + + func _ready(): + # Get the JavaScript `window` object. + var window = JavaScriptBridge.get_interface("window") + # Set the `window.onbeforeunload` DOM event listener. + window.onbeforeunload = _callback_ref + + func _my_callback(args): + # Get the first argument (the DOM event in our case). + var js_event = args[0] + # Call preventDefault and set the `returnValue` property of the DOM event. + js_event.preventDefault() + js_event.returnValue = '' + +Here is another example that asks the user for the `Notification permission `__ +and waits asynchronously to deliver a notification if the permission is +granted: + +.. code-block:: gdscript + + extends Node + + # Here we create a reference to the `_on_permissions` function (below). + # This reference will be kept until the node is freed. + var _permission_callback = JavaScriptBridge.create_callback(_on_permissions) + + func _ready(): + # NOTE: This is done in `_ready` for simplicity, but SHOULD BE done in response + # to user input instead (e.g. during `_input`, or `button_pressed` event, etc.), + # otherwise it might not work. + + # Get the `window.Notification` JavaScript object. + var notification = JavaScriptBridge.get_interface("Notification") + # Call the `window.Notification.requestPermission` method which returns a JavaScript + # Promise, and bind our callback to it. + notification.requestPermission().then(_permission_callback) + + func _on_permissions(args): + # The first argument of this callback is the string "granted" if the permission is granted. + var permission = args[0] + if permission == "granted": + print("Permission granted, sending notification.") + # Create the notification: `new Notification("Hi there!")` + JavaScriptBridge.create_object("Notification", "Hi there!") + else: + print("No notification permission.") + +Can I use my favorite library? +------------------------------ + +You most likely can. First, you have to +include your library in the page. You can simply customize the +:ref:`Head Include ` during export (see below), +or even :ref:`write your own template `. + +In the example below, we customize the ``Head Include`` to add an external library +(`axios `__) from a content delivery network, and a +second `` + + + +We can then access both the library and the function from Godot, like we did in +previous examples: + +.. code-block:: gdscript + + extends Node + + # Here create a reference to the `_on_get` function (below). + # This reference will be kept until the node is freed. + var _callback = JavaScriptBridge.create_callback(_on_get) + + func _ready(): + # Get the `window` object, where globally defined functions are. + var window = JavaScriptBridge.get_interface("window") + # Call the JavaScript `myFunc` function defined in the custom HTML head. + window.myFunc() + # Get the `axios` library (loaded from a CDN in the custom HTML head). + var axios = JavaScriptBridge.get_interface("axios") + # Make a GET request to the current location, and receive the callback when done. + axios.get(window.location.toString()).then(_callback) + + func _on_get(args): + OS.alert("On Get") + + +The eval interface +------------------ + +The ``eval`` method works similarly to the JavaScript function of the same +name. It takes a string as an argument and executes it as JavaScript code. +This allows interacting with the browser in ways not possible with script +languages integrated into Godot. + +.. tabs:: + .. code-tab:: gdscript + + func my_func(): + JavaScriptBridge.eval("alert('Calling JavaScript per GDScript!');") + + .. code-tab:: csharp + + private void MyFunc() + { + JavaScriptBridge.Eval("alert('Calling JavaScript per C#!');") + } + +The value of the last JavaScript statement is converted to a GDScript value and +returned by ``eval()`` under certain circumstances: + + * JavaScript ``number`` is returned as :ref:`class_float` + * JavaScript ``boolean`` is returned as :ref:`class_bool` + * JavaScript ``string`` is returned as :ref:`class_String` + * JavaScript ``ArrayBuffer``, ``TypedArray``, and ``DataView`` are returned as :ref:`PackedByteArray` + +.. tabs:: + .. code-tab:: gdscript + + func my_func2(): + var js_return = JavaScriptBridge.eval("var myNumber = 1; myNumber + 2;") + print(js_return) # prints '3.0' + + .. code-tab:: csharp + + private void MyFunc2() + { + var jsReturn = JavaScriptBridge.Eval("var myNumber = 1; myNumber + 2;"); + GD.Print(jsReturn); // prints '3.0' + } + +Any other JavaScript value is returned as ``null``. + +HTML5 export templates may be :ref:`built ` without +support for the singleton to improve security. With such templates, and on +platforms other than HTML5, calling ``JavaScriptBridge.eval`` will also return +``null``. The availability of the singleton can be checked with the +``web`` :ref:`feature tag `: + +.. tabs:: + .. code-tab:: gdscript + + func my_func3(): + if OS.has_feature('web'): + JavaScriptBridge.eval(""" + console.log('The JavaScriptBridge singleton is available') + """) + else: + print("The JavaScriptBridge singleton is NOT available") + + .. code-tab:: csharp + + private void MyFunc3() + { + if (OS.HasFeature("web")) + { + JavaScriptBridge.Eval("console.log('The JavaScriptBridge singleton is available')"); + } + else + { + GD.Print("The JavaScriptBridge singleton is NOT available"); + } + } + +.. tip:: GDScript's multi-line strings, surrounded by 3 quotes ``"""`` as in + ``my_func3()`` above, are useful to keep JavaScript code readable. + +The ``eval`` method also accepts a second, optional Boolean argument, which +specifies whether to execute the code in the global execution context, +defaulting to ``false`` to prevent polluting the global namespace: + +.. tabs:: + .. code-tab:: gdscript + + func my_func4(): + # execute in global execution context, + # thus adding a new JavaScript global variable `SomeGlobal` + JavaScriptBridge.eval("var SomeGlobal = {};", true) + + .. code-tab:: csharp + + private void MyFunc4() + { + // execute in global execution context, + // thus adding a new JavaScript global variable `SomeGlobal` + JavaScriptBridge.Eval("var SomeGlobal = {};", true); + } + + +.. _doc_web_downloading_files: + +Downloading files +----------------- + +Downloading files (e.g. a save game) from the Godot Web export to the user's computer can be done by directly interacting with JavaScript, but given it is a +very common use case, Godot exposes this functionality to scripting via +a dedicated :ref:`JavaScriptBridge.download_buffer() ` +function which lets you download any generated buffer. + +Here is a minimal example on how to use it: + +extends Node + +.. code-block:: gdscript + + func _ready(): + # Asks the user download a file called "hello.txt" whose content will be the string "Hello". + JavaScriptBridge.download_buffer("Hello".to_utf8_buffer(), "hello.txt") + +And here is a more complete example on how to download a previously saved file: + +.. code-block:: gdscript + + extends Node + + # Open a file for reading and download it via the JavaScript singleton. + func _download_file(path): + var file = FileAccess.open(path, FileAccess.READ) + if file == null: + push_error("Failed to load file") + return + # Get the file name. + var fname = path.get_file() + # Read the whole file to memory. + var buffer = file.get_buffer(file.get_len()) + # Prompt the user to download the file (will have the same name as the input file). + JavaScriptBridge.download_buffer(buffer, fname) + + func _ready(): + # Create a temporary file. + var config = ConfigFile.new() + config.set_value("option", "one", false) + config.save("/tmp/test.cfg") + + # Download it + _download_file("/tmp/test.cfg")