Browse Source

Merge pull request #8986 from Faless/web/javascript_bridge

[Web] Expand "Interacting with Javascript"
Max Hilbrunner 1 year ago
parent
commit
e3e0db3b92

+ 1 - 1
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_release
     scons platform=web target=template_debug
     scons platform=web target=template_debug
 
 
-By default, the :ref:`JavaScript singleton <doc_javascript_eval>` will be built
+By default, the :ref:`JavaScriptBridge singleton <doc_web_javascript_bridge>` will be built
 into the engine. Official export templates also have the JavaScript singleton
 into the engine. Official export templates also have the JavaScript singleton
 enabled. Since ``eval()`` calls can be a security concern, the
 enabled. Since ``eval()`` calls can be a security concern, the
 ``javascript_eval`` option can be used to build without the singleton::
 ``javascript_eval`` option can be used to build without the singleton::

+ 4 - 103
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
     be used. Instead, you should use an established web server such as Apache or
     nginx.
     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<class_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 <doc_compiling_for_web>` 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 <doc_feature_tags>`:
-
-.. 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 <doc_web_javascript_bridge>` on how to interact with JavaScript and access some unique Web browser features.
 
 
 Environment variables
 Environment variables
 ---------------------
 ---------------------

+ 1 - 1
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.
     exported to HTML5 on a mobile device.
 
 
     To check whether a project exported to HTML5 is running on a mobile device,
     To check whether a project exported to HTML5 is running on a mobile device,
-    :ref:`call JavaScript code <doc_javascript_eval>` that reads the browser's
+    :ref:`call JavaScript code <doc_web_javascript_bridge>` that reads the browser's
     user agent.
     user agent.
 
 
 Custom features
 Custom features

+ 4 - 3
tutorials/platform/web/index.rst

@@ -1,14 +1,15 @@
 :allow_comments: False
 :allow_comments: False
 :article_outdated: True
 :article_outdated: True
 
 
-.. _doc_platform_html5:
+.. _doc_platform_web:
 
 
-HTML5
-=====
+Web
+===
 
 
 .. toctree::
 .. toctree::
    :maxdepth: 1
    :maxdepth: 1
    :name: toc-learn-features-platform-html5
    :name: toc-learn-features-platform-html5
 
 
+   javascript_bridge
    html5_shell_classref
    html5_shell_classref
    customizing_html5_shell
    customizing_html5_shell

+ 325 - 0
tutorials/platform/web/javascript_bridge.rst

@@ -0,0 +1,325 @@
+.. _doc_web_javascript_bridge:
+
+The JavaScriptBridge Singleton
+==============================
+
+In web builds, the :ref:`JavaScriptBridge <class_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 <class_JavaScriptObject>` that tries to feel
+natural in the context of Godot scripting (e.g. GDScript and C#).
+
+The :ref:`JavaScriptBridge.get_interface() <class_JavaScriptBridge_method_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() <class_JavaScriptBridge_method_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 <https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API>`__
+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 <doc_javascript_export_options>` during export (see below),
+or even :ref:`write your own template <doc_customizing_html5_shell>`.
+
+In the example below, we customize the ``Head Include`` to add an external library
+(`axios <https://axios-http.com/>`__) from a content delivery network, and a
+second ``<script>`` tag to define our own custom function:
+
+.. code-block:: html
+
+    <!-- Axios -->
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <!-- Custom function -->
+    <script>
+    function myFunc() {
+        alert("My func!");
+    }
+    </script>
+
+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<class_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 <doc_compiling_for_web>` 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 <doc_feature_tags>`:
+
+.. 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() <class_JavaScriptBridge_method_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")