EditorUtility.OpenFilePanel for Unity WebGL (Runtime)

This may sound like something simple, but actually it is quite complicated to do, and the reason is that WebGL build runs in browser, and is subject to many security restrictions, among others limiting its access to local file system. It is possible to do though, in a hacky way.

The idea is to use HTML file input to open the file browse dialog. We can call it from Unity code using ExternalEval, see more here: http://docs.unity3d.com/Manual/UnityWebPlayerandbrowsercommunication.html http://docs.unity3d.com/ScriptReference/Application.ExternalEval.html

Yet, it is not that easy. The problem is that all modern browsers allow to show files dialog only as a result to user click event, as a security restriction, and you can't do anything about it.

Ok, so we can create a button, and open file dialog on click, this will work, right? WRONG. If we simply create unity button and handle click - this will not work, since Unity has its own event management, it is synchronized with frame rate, so the event will occur only when the actual javascript event is over. It is almost same problem like described here, http://docs.unity3d.com/Manual/webgl-cursorfullscreen.html except Unity has no good built in solution.

So here is the hack: click is mouse down + mouse up, right? We add click listener to HTML document, then in unity we listen to mouse down on our button. When it is down, we know that next UP will be click, so we mark some flag in HTML document to remember it. Then, when we get click in document, we can look at this flag and conclude our button was clicked. Then we call javascript function that opens file dialog, and we send results back to Unity using SendMessage http://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html. Finally.

But wait, there is more. The problem is that we can't simply get file path when running in browser. Our application is not allowed to get any info about user's computer, again, security restriction. The best we can do is get a blob url using URL.CreateObjectURL which will work on most browsers, http://caniuse.com/#search=createobjecturl

We can use WWW class to retrieve data from it, just remember that this URL is accessible only from within your application scope.

So with all these, the solution is very hacky, but possible. Here is an example code that allows user to select an image, and set it as material texture.

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;

public class OpenFileDialog : MonoBehaviour, IPointerDownHandler {

    public Renderer preview;
    public Text text;

    void Start() {
        Application.ExternalEval(
            @"
document.addEventListener('click', function() {

    var fileuploader = document.getElementById('fileuploader');
    if (!fileuploader) {
        fileuploader = document.createElement('input');
        fileuploader.setAttribute('style','display:none;');
        fileuploader.setAttribute('type', 'file');
        fileuploader.setAttribute('id', 'fileuploader');
        fileuploader.setAttribute('class', 'focused');
        document.getElementsByTagName('body')[0].appendChild(fileuploader);

        fileuploader.onchange = function(e) {
        var files = e.target.files;
            for (var i = 0, f; f = files[i]; i++) {
                window.alert(URL.createObjectURL(f));
                SendMessage('" + gameObject.name +@"', 'FileDialogResult', URL.createObjectURL(f));
            }
        };
    }
    if (fileuploader.getAttribute('class') == 'focused') {
        fileuploader.setAttribute('class', '');
        fileuploader.click();
    }
});
            ");
    }

    public void OnPointerDown (PointerEventData eventData)  {
        Application.ExternalEval(
            @"
var fileuploader = document.getElementById('fileuploader');
if (fileuploader) {
    fileuploader.setAttribute('class', 'focused');
}
            ");
    }

    public void FileDialogResult(string fileUrl) {
        Debug.Log(fileUrl);
        text.text = fileUrl;
        StartCoroutine(PreviewCoroutine(fileUrl));
    }

    IEnumerator PreviewCoroutine(string url) {
        var www = new WWW(url);
        yield return www;
        preview.material.mainTexture = www.texture;
    }
}

If someone manages to finds a simpler way please share, but I really doubt it is possible. Hope this helps.


Wow. Yuri Nudelman's solution is impressive black-magic, and it is still the best solution to be found. BUT: Both WWW-class and ExternalEval are deprecated by now.

It does run (with warnings) but will stop working in the not to distant future.

So, to help anybody who wants to implement this:

Both Javascript-functions must be put in a .jslib inside of a "Plugins" folder. First one like this:

mergeInto(
  LibraryManager.library,
  {
    AddClickListenerForFileDialog: function () {
      document.addEventListener('click', function () {

        var fileuploader = document.getElementById('fileuploader');
        if (!fileuploader) {
          fileuploader = document.createElement('input');
          fileuploader.setAttribute('style', 'display:none;');
          fileuploader.setAttribute('type', 'file');
          fileuploader.setAttribute('id', 'fileuploader');
          fileuploader.setAttribute('class', '');
          document.getElementsByTagName('body')[0].appendChild(fileuploader);

          fileuploader.onchange = function (e) {
            var files = e.target.files;
            for (var i = 0, f; f = files[i]; i++) {
              window.alert(URL.createObjectURL(f));
              SendMessage('BrowserFileLoading', 'FileDialogResult', URL.createObjectURL(f));
            }
          };
        }
        if (fileuploader.getAttribute('class') == 'focused') {
          fileuploader.setAttribute('class', '');
          fileuploader.click();
        }
      });
    }
  }
);

Do note that I added two changes: a) I removed the 'focused'. This prevents the script triggering at the start of the program:

   fileuploader.setAttribute('class', '');

b) I added the name of my Unity GameObject by hand. This must be the same as the GameObject you put your (unity-) script on:

   SendMessage('BrowserFileLoading', 'FileDialogResult', URL.createObjectURL(f));

You can call this external function using:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using UnityEngine.Networking;
using System.Runtime.InteropServices;
public class BrowserFileLoadingDialog : MonoBehaviour
{
  [DllImport("__Internal")] private static extern void AddClickListenerForFileDialog();

  void Start()
  {
    AddClickListenerForFileDialog();
  }

  public void FileDialogResult(string fileUrl)
  {
    Debug.Log(fileUrl);
    UrlTextField.text = fileUrl;
    StartCoroutine(LoadBlob(fileUrl));
  }

  IEnumerator LoadBlob(string url)
  {
    UnityWebRequest webRequest = UnityWebRequest.Get(url);
    yield return webRequest.SendWebRequest();

    if (!webRequest.isNetworkError && !webRequest.isHttpError)
    {
      // Get text content like this:
      Debug.Log(webRequest.downloadHandler.text);

    }
}

The second script (can be put into the same .jslib-file) looks like this:

mergeInto(
  LibraryManager.library,
  {
    FocusFileUploader: function () {
      var fileuploader = document.getElementById('fileuploader');
      if (fileuploader) {
          fileuploader.setAttribute('class', 'focused');
      }
    }
  }
);

No big changes here, is used like the one above and should (just like Yuri Nudelman suggested) be called on CursorDown.