0
votes

I am looking to load a Google Drive file picker by clicking a button within a Google Form add-on sidebar.

Problem

I have not been able to work out how to load the picker (in a modal dialog) directly from the sidebar, then callback the document id to the sidebar.

I have been able to successfully load a modal dialog and then load the picker from the modal dialog, however am struggling to understand how to load the picker directly from the sidebar.

Any guidance is appreciated. My current code is shown below.

.gs file:

function onOpen(e) {
  FormApp.getUi()
  .createAddonMenu()
  .addItem('Picker', 'showSidebar')
  .addToUi();
}

function showSidebar(){

    var ui = HtmlService.createHtmlOutputFromFile('Sidebar')
  .setSandboxMode(HtmlService.SandboxMode.IFRAME)
  .setTitle('Title');
  FormApp.getUi().showSidebar(ui);

}

function openPicker(){

  var html = HtmlService.createHtmlOutputFromFile('Picker')
  .setWidth(600)
  .setHeight(425)
  .setSandboxMode(HtmlService.SandboxMode.IFRAME);
  FormApp.getUi().showModalDialog(html, 'Select a file');

}


function getOAuthToken() {
  DriveApp.getRootFolder();
  return ScriptApp.getOAuthToken();
}

sidebar html

<!DOCTYPE html>
<html>
  <head>
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
    <base target="_top">
  </head>
  <body>
    <button onclick='openPicker()'>Select a file</button>
  </body>
</html>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script>


function openPicker() {
google.script.run
.withSuccessHandler(success)
.withFailureHandler(failure)
.openPicker();
}

function success() {
console.log('success');
}

function failure() {
console.log('failure');
}

</script>

picker html

 <!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
  <script>

    var DEVELOPER_KEY = '';
    var DIALOG_DIMENSIONS = {width: 600, height: 425};
    var pickerApiLoaded = false;


    function onApiLoad() {
      gapi.load('picker', {'callback': function() {
        pickerApiLoaded = true;
      }});
     }


    function getOAuthToken() {
      google.script.run
      .withSuccessHandler(createPicker)
      .withFailureHandler(showError)
      .getOAuthToken();
    }


    function createPicker(token) {
    console.log("here");
      if (pickerApiLoaded && token) {
        var picker = new google.picker.PickerBuilder()
            // Instruct Picker to display only spreadsheets in Drive. For other
            // views, see https://developers.google.com/picker/docs/#otherviews
            .addView(google.picker.ViewId.DOCUMENTS)
            // Hide the navigation panel so that Picker fills more of the dialog.
            .enableFeature(google.picker.Feature.NAV_HIDDEN)
            // Hide the title bar since an Apps Script dialog already has a title.
            .hideTitleBar()
            .setOAuthToken(token)
            .setDeveloperKey(DEVELOPER_KEY)
            .setCallback(pickerCallback)
            .setOrigin(google.script.host.origin)
            // Instruct Picker to fill the dialog, minus 2 pixels for the border.
            .setSize(DIALOG_DIMENSIONS.width - 2,
                DIALOG_DIMENSIONS.height - 2)
            .build();
        picker.setVisible(true);
      } else {
        showError('Unable to load the file picker.');
      }
    }


    function pickerCallback(data) {
      var action = data[google.picker.Response.ACTION];
      if (action == google.picker.Action.PICKED) {
        var doc = data[google.picker.Response.DOCUMENTS][0];
        var id = doc[google.picker.Document.ID];
        var url = doc[google.picker.Document.URL];
        var title = doc[google.picker.Document.NAME];
        document.getElementById('result').innerHTML =
            '<b>You chose:</b><br>Name: <a href="' + url + '">' + title +
            '</a><br>ID: ' + id;
      } else if (action == google.picker.Action.CANCEL) {
        document.getElementById('result').innerHTML = 'Picker canceled.';
      }
    }


    function showError(message) {
      document.getElementById('result').innerHTML = 'Error: ' + message;
    }
  </script>
</head>
<body>
  <div>
    <button onclick='getOAuthToken()'>Select a file</button>
    <p id='result'></p>
  </div>
  <script src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>
2
Sorry for my previous comment - I didn't read the question in its entirety. You could simply create and serve a single html page as a sidebar and move the client-side JS to that page (token request, etc). Perhaps I'm missing something.Anton Dementiev
@Anton Dementiev, when I tried to call a single html page it was loading the picker within the sidebar rather than opening it in a new dialog window.beano
@AntonDementiev I think you were more on the right track than the solution I accepted. I realise now, having separate two html pages creates issues getting the document id back to the sidebar through the callback function. Any guidance on what I need to move between the files?beano

2 Answers

1
votes

Here's the quick and dirty solution I came up with. My '.gs' file contains functions for displaying UI containers served via HtmlService and obtaining the OAuth token. Note that you can pass data models to HtmlTemplate objects before they get converted to raw HTML and passed to the client. When we call the 'showSidebar' function from the custom menu, no arguments are passed so it's the first call. If 'params' is not 'undefined', we know the call comes from the modal dialog.

Code.gs

var ui = SpreadsheetApp.getUi()


//add custom menu to the spreadsheet;
function onOpen() {

ui.createMenu('Sidebar')
.addItem('Show sidebar', 'showSidebar')
.addToUi();

}

function showSidebar(params){

var sidebar = HtmlService.createTemplateFromFile('sidebar');
var model = "your selection"; //default message to be displayed

  if (params) { //if arguments are passed, it's a callback from modal dialog
    model = "You selected document with id: " +     
                  params.id + ", title: " + params.title + 
                  " url: " + params.url; 
  }  

sidebar.model = model; // pass model to the template
var htmlOutput = sidebar.evaluate(); //execute inline scriplets from the template to complete DOM construction. 
ui.showSidebar(htmlOutput); //pass resulting UI object to sidebar container

}

function showModalDialog() {

// produce HtmlOutput for modal dialog
var modalDialog = HtmlService.createTemplateFromFile('modal_dialog')
                             .evaluate()
                             .setWidth(600)
                             .setHeight(425)
                             .setSandboxMode(HtmlService.SandboxMode.IFRAME); 

ui.showModalDialog(modalDialog, 'Picker');


}

function getOAuthToken() {
  DriveApp.getRootFolder();
  return ScriptApp.getOAuthToken();
}

sidebar.html:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
  <input type="button" id="button" value="Open picker">
  <div>  Selection:  <?!=  model ?> </div>  

  <script>

  window.onload = (function(){

  var button = document.getElementById('button');
  button.addEventListener('click', function(event){

  google.script.run
               .withSuccessHandler(function(){             

                 google.script.host.close();             

               })
               .showModalDialog(); 

  });


  })();  
  </script>
  </body>
</html>

The scriptlets between <? ?> will fire when 'evaluate()' is called on the object belonging to the HtmlTemplate class. There are several types of scriptlets - more on scriptlets here:

https://developers.google.com/apps-script/guides/html/templates#scriptlets

Adding '!' immediately after the first '?' will force-print the result to the page. After the DOM tree is loaded, I add the event listener that executes the 'showDialog()' function in '.gs' file when user clicks the button.

The template for the dialog window is from the picker quickstart here

https://developers.google.com/apps-script/guides/dialogs

I modified the 'onApiLoaded' function to display the picker immediately upon loading.

 function onApiLoad() {
      gapi.load('picker', {'callback': function() {
        pickerApiLoaded = true;
        getOAuthToken();

      }});
     }

Finally, I added the following code to the 'pickerCallback' function.

if (action == google.picker.Action.PICKED) {
        var doc = data[google.picker.Response.DOCUMENTS][0];
        var id = doc[google.picker.Document.ID];
        var url = doc[google.picker.Document.URL];
        var title = doc[google.picker.Document.NAME];

        google.script.run.withSuccessHandler(function(){

           google.script.host.close();

        }).showSidebar({id: id, url: url, title: title});

When user makes a selection, the data gets passed as parameter to the 'showSideBar' function in '.gs' file. Since 'google.script.host(run)' calls are asynchronous, I placed the window-closing call inside 'withSuccessHandler' to prevent early closure.

Result: Selection data is displayed in the sidebar.

There's another approach that I like better. In GAS, even file-bound scripts can be published as web apps, so you can basically create multi-page apps by making calls via UrlFetchApp.fetch(). For example, you can define one master page containing a single 'container' div. You may also store all JS code and load the APIs on the master page. If you need to navigate to another page, you can simply query the web app via fetch(url) and paste the resulting html into container on the master page via

containerDiv.innerHTML = html;

The url for the web app can be dynamically obtained by calling ScriptApp.getService().getUrl(). However, the script must first be published as a web app that is anonymously accessible. Hope this helps.

0
votes

Just modify your picker Html code to automatically run getOAuthToken() functions when the html on modal dialog loads, like so:

... Code till here remains as above
<body onload = 'getOAuthToken()'>
  <div>
    <p id='result'></p>
  </div>
  <script src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>

For more details on onload event refer here

Hope that helps.