Close dialog when both dialog and custom sidebar are present - google-apps-script

I'm having trouble with the google.script.host.close() command in that it is closing my sidebar rather than the dialog.
function clickWeek() {
setWeekColor('week', '#7FFF00');
setMonthColor('month', '#d6d6c2');
setPressColor('press', '#d6d6c2');
setAllDataColor('allData', '#d6d6c2');
google.script.run.withSuccessHandler(google.script.host.close).weekAheadCreate();
}
The dialog is opened from the top of the weekAheadCreate() fucntion as per below:
var htmlOutput = HtmlService
.createHtmlOutput('<p>Please wait a moment...</p>')
.setWidth(250)
.setHeight(80);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Loading');
Any ideas how I can force the google.script.host.close() to act on the dialog rather than the sidebar?

The close() method closes the current dialog/sidebar, and asuming the clickWeek() is in your sidebar, it will close it instead of the dialog. You need to run the close() method inside the dialog.
How about you fire your server-side function when the dialog is created and when the withSuccessHandler() detects a successful return then it closes the dialog using close().
You will need to use createHtmlOutputFromFile when creating the dialog insted of createHtmlOutput and inside your html use the <script> tag. Here's an example:
code.gs
function createDialog(){
var htmlOutput = HtmlService
.createHtmlOutputFromFile('dialog')
.setWidth(250)
.setHeight(80);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Loading');
}
function doBusyStuff(){
// Busy stuff
}
dialog.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
(function() {
// Runs the busy stuff and closes the dialog using .close() on success
google.script.run
.withSuccessHandler(google.script.host.close)
.doBusyStuff();
})();
</script>
</head>
<body>
<p>Please wait a moment...</p>
</body>
</html>

Related

Control Flow Between Server and Client and Back to Server for Google Apps Script

I have a question about the control flow between some JavaScript code running as bound functions within a google spreadsheet - so server side - and a dialog (that happens to be Modal, but Modeless is the same) that is client side.
While the code examples below work fine in that the dialog successfully calls the server side function as per the line below, and the withSuccessHandler works too.
google.script.run.withSuccessHandler(success_callback).getCredentials(this.parentNode)
But what I actually want to achieve is for some server side code to carry on executing once the dialog has gone; ideally from the point the .showModalDialog() function was called, but I'd be happy just passing control back to any server-side function.
Some example software is below; don't forget this works, just not how I want it too! Essentially the event handler for a menu item created by the the OnOpen() function calls a modal dialog to prompt the user for security credentials.
var html = HtmlService.createHtmlOutputFromFile('authorization_dialog');
SpreadsheetApp.getUi()
.showModalDialog(html, 'Authorization Dialog');
The HTML file:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form>
Authorization Code:
<input type="text" name="authorization_code"><br><br>
Account ID:
<input type="text" name="account_id"><br><br>
Enter account details...
<br>
<br><br>
<input type="button" value="OK"
onclick="google.script.run.withSuccessHandler(success_callback).getCredentials(this.parentNode)" />
<input type="button" value="Close"
onclick="google.script.host.close()" />
</form>
<script>
// Using this call back prevents the need to hit the Close Button after OK.
function success_callback() {
google.script.host.close(); // Close the dialog.
}
</script>
</body>
</html>
If you don't need a response from the server-side function, simply omit 'withSuccessHandler';
function func_client(){
google.script.run.func_server();
google.script.host.close();
}
In this case, the server-side code will continue executing without locking your client's UI - you can call any other functions inside 'func_server'.
If you'd like to process a response from the first function call, call the second function from 'success_callback'. The dialog will be closed without waiting for the google.script.run to complete, but the server code will continue executing.
In the example below, the 1st server function passes form data back to the client where 'success_callback' immediately invokes another server function that takes a while to complete (it logs each file in my Google Drive);
Client:
<form id="form">
.....
<input type="submit" id="ok" value="OK" />
<input type="button" value="Close" onclick="google.script.host.close()" />
.....
</form>
<script>
window.onload = function(){
document.getElementById("form")
.addEventListener("submit", function(e){
e.preventDefault();
google.script.run
.withSuccessHandler(success_callback)
.logFormData(this);
});
}
function success_callback(response) {
console.log(response);
google.script.run.scanFiles();
google.script.host.close(); // Close the dialog.
}
</script>
Server:
function showDialog(){
var ui = SpreadsheetApp.getUi();
//IMPORTANT: client-side scripts won't be executed
//without calling evaluate() on the HtmlTemplate object before passing it to UI;
var template = HtmlService.createTemplateFromFile("dialog");
var html = template.evaluate();
ui.showModalDialog(html, "dialog");
}
function logFormData(formData){
return formData;
}
function scanFiles() {
var files = DriveApp.getFiles();
var file;
while (files.hasNext()) {
file = files.next();
Logger.log(file.getName() + ": " + file.getMimeType());
}
}

Google apps script get user input with no length limit

I want to take user input (HTML specifically) using either:
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('Paste HTML below');
or
var input = Browser.inputBox('Paste HTML below', Browser.Buttons.OK_CANCEL);
These work fine for small inputs, however when copying over the entire HTML for a page of interest an error occurs (in each case). This error cannot be caught, it simply crashes the script.
Do you know why this is happening? I can't find anything in the docs that mention limits on input size.
Any experience doing this a different way?
Edit: as per a suggestion in the comments, I have tried another method (below). This also fails (with no error message) when passed large input.
First I set up Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
Paste Sitemap Content Below
<textarea id="user-input-box" rows="4" cols="50"></textarea>
<script>
function logToConsole() {
var userInput = document.getElementById("user-input-box").value;
google.script.run.doSomething(userInput);
}
</script>
<input type="button" value="Close" onclick="logToConsole();google.script.host.close();" />
</body>
</html>
Then in file Code.gs
function testDialog() {
var html = HtmlService.createHtmlOutputFromFile('Page')
.setWidth(400)
.setHeight(300);
SpreadsheetApp.getUi()
.showModalDialog(html, 'My custom dialog');
}
function doSomething(userInput){
Logger.log(userInput);
}
I just ran into the same problem and couldn't log the error. In my case as is yours, you're calling your logToConsole() function and then directly after you're closing the dialog by using google.script.host.close();
google.script.host.close() is the problem. For some reason it can cancel the script execution - this typically happens when you're sending a lot of data back. The trick is to use a successHandler when you call your script which then calls google.script.host.close(). This way, the data transfer from the dialog finishes correctly and when you call withSuccessHandler(), that callback closes the dialog. Try this amendment to your code:
<script>
function logToConsole() {
var userInput = document.getElementById("user-input-box").value;
google.script.run.withSuccessHandler(closeDialog).doSomething(userInput);
}
function closeDialog() {
google.script.host.close();
}
</script>
<input type="button" value="Close" onclick="logToConsole()" />

Create dropdown menu within a popup window

I have a list of records in multiple sheets (same workbook).
I currently have a dropdown menu within my googlesheet where if you select one of the records it will delete the row with that record.
However, I would like to give the option to either move it to another sheet or delete it. I was trying to use UiApp but then found out alot of the options are deprecated and that now I have to use HTMLService.
So what I'm looking to do is, once I select a record, have a popup that has two options.
Option 1 : a Move option (button) with a dropdown of the names of the other sheets within the workbook that will then move that record to that sheet
Option 2 : Delete the record
Option 3 : Cancel.
Is this possible? and if so, would someone be able to guide me to the right direction or a similar example so I can try and figure out how to get that going?
You can try creating a Custom dialogs
A custom dialog can display an HTML service user interface inside a Google Docs, Sheets, or Forms editor.
Custom dialogs do not suspend the server-side script while the dialog is open. The client-side component can make asynchronous calls to the server-side script using either the google.script API for HTML-service interfaces or server handlers for UI-service interfaces.
Code.gs
function onOpen() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Custom Menu')
.addItem('Show dialog', 'showDialog')
.addToUi();
}
function showDialog() {
var html = HtmlService.createHtmlOutputFromFile('Page')
.setWidth(400)
.setHeight(300);
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.showModalDialog(html, 'My custom dialog');
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<select>
<option>Delete</option>
<option>Move</option>
</select>
</body>
</html>
With that, try reading about HTML Service: Communicate with Server Functions
google.script.run is an asynchronous client-side JavaScript API that allows HTML-service pages to call server-side Apps Script functions. The following example shows the most basic functionality of google.script.run — calling a function on the server from client-side JavaScript.
Here is a sample code for form:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
// Prevent forms from submitting.
function preventFormSubmit() {
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
forms[i].addEventListener('submit', function(event) {
event.preventDefault();
});
}
}
window.addEventListener('load', preventFormSubmit);
function handleFormSubmit(formObject) {
google.script.run.withSuccessHandler(updateUrl).processForm(formObject);
}
function updateUrl(url) {
var div = document.getElementById('output');
div.innerHTML = 'Got it!';
}
</script>
</head>
<body>
<form id="myForm" onsubmit="handleFormSubmit(this)">
<input name="myFile" type="file" />
<input type="submit" value="Submit" />
</form>
<div id="output"></div>
</body>
</html>
Hope this helps!

Converting from NATIVE to IFRAME sandbox

I have a large application that I want to convert from NATIVE to IFRAME sandbox now that NATIVE is deprecated. The general flow of the application is as follows: The user fills out a form on the beginning page and presses a Begin button. The beginning page is then hidden, and based upon values from the first page, the user is then shown a new page. My problem when using IFRAME is that the new page is never shown. It works as expected in NATIVE mode. I have created a simplified script that exhibits the problem. Please help me understand what I am forgetting or doing wrong.
Code.gs
function doGet() {
Logger.log('enter doget');
var html = HtmlService.createTemplateFromFile('BeginHeader').evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
return html;
}
function include(filename) {
Logger.log('enter include');
Logger.log(filename);
var html = HtmlService.createHtmlOutputFromFile(filename).getContent();
Logger.log(html);
return html;
}
Javascript.html
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script
src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js">
</script>
<script
src="https://apis.google.com/js/api.js?onload=onApiLoad">
</script>
<script>
function showForm(hdr) {
console.log('enter showform');
console.log(hdr);
console.log('hiding first page');
document.getElementById('beginDiv').style.display = 'none';
var el = document.getElementById('recordDiv');
el.innerHTML = hdr;
console.log('showing new page');
el.style.display = 'block';
}
function oops(error) {
console.log('entered oops');
alert(error.message);
}
</script>
<script>
$(document).ready(function() {
console.log('begin ready');
$("#beginForm").submit(function() {
console.log('enter begin submit');
//console.log('hiding first page');
//document.getElementById('beginDiv').style.display = 'none';
console.log('including page 2');
google.script.run
.withSuccessHandler(showForm)
.withFailureHandler(oops)
.include('Page2');
});
});
</script>
BeginHeader.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<div id="beginDiv" style="display:block">
<p>Click on Begin. </p>
<form id="beginForm">
<input type="submit" value="Begin">
</form>
</div>
<!-- results of content being filled in -->
<div id="recordDiv"></div>
<?!= include('Javascript'); ?>
</body>
</html>
Page2.html
<!DOCTYPE html>
<html>
<body>
<p> This is page 2. </p>
</body>
</html>
There is no point in ever using a button of the "submit" type, unless you want to force the form to make an HTTP Request, and reload the application. That's what a "submit" type button does. It causes the page to be reloaded. The "submit" type button is meant to work together with a form in a certain way. It causes a GET or POST request to happen. That's what the problem is. So, you'll need to reconfigure things a little bit.
Just use a plain button.
<input type="button" value="Begin" onmouseup="gotoPg2()">
I created a gotoPg2() function to test it:
<script>
window.gotoPg2 = function() {
console.log('enter begin submit');
//console.log('hiding first page');
//document.getElementById('beginDiv').style.display = 'none';
console.log('including page 2');
google.script.run
.withSuccessHandler(showForm)
.withFailureHandler(oops)
.include('Page2');
};
</script>
If you use that, they you don't need the $(document).ready(function() { etc. code anymore. And, if you don't need that code, then you don't need to load jQuery.
Unless you are using jQuery for other things, then you don't need:
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script
src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js">
</script>
The NATIVE mode was probably blocking the intended usage of the "submit" request. That's why the code in NATIVE was working. IFRAME allows things to work as they are built and intended to work, which means that the page was probably trying to be reloaded, and an error was occurring. I was getting a 404 page error in the browser console.

In Firefox add-on, completion of any event handler in tab's content script triggers tab's "ready" event

In a Firefox add-on, completion of any event handler in my tab's content script triggers tab's "ready" event.
I'm trying to figure out if it's possible to use a content script to alter the contents of a tab (which originally contains an HTML file stored locally in the add-on's data folder), and later re-visit the tab for further modifications. Unfortunately for me, the page in the tab seems to revert to the original HTML file (as evidenced by the output of line 6 in data/demo.js) and to re-attach the content script,
lib/main.js:
exports.main = function() {
var data = require("sdk/self").data;
// Create a widget that will open a tab:
require("sdk/widget").Widget({
id: "listener",
label: "listener demo",
content: "?",
onClick: function() {
// Open tab:
require("sdk/tabs").open({
url: data.url("demo.html"),
onReady: function(tab) {
var worker = tab.attach({
// Content script is re-run following EITHER button
// being pressed (whether or not 'demo' event listener
// is activated):
contentScriptFile: data.url("demo.js")
});
worker.port.on("demo-dun", function(message) {
// This message is output following pressing button
// with listener attached by content script:
console.log("'demo-dun' emitted message '" +
message + "'");
});
// Sample text below is received by content script every
// time content script is run, which is to say, every time
// either button is pressed.
worker.port.on("initialized", function () {
worker.port.emit("demo", "sample text");
});
}
});
}
});
};
data/demo.html (the tab's content):
<!DOCTYPE HTML>
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
<title>Listner demo</title>
</head>
<!-- The content script will remove the class from the body element. -->
<body class="uninitialized">
<form>
<!-- The following button has a click listener from the start -->
<button onclick="alert('hard coded event listener')">press me for hard-coded event listener</button>
<!-- An event listener will be added to the following button by the content script -->
<button id="demo">press me for event listener attached by content script</button>
</form>
</body>
</html>
data/demo.js (the tab's contentScriptFile):
// content script is re-entered after pressing either button:
alert("entered content script");
// "initialized" event will be emitted once for each tab, IN THEORY.
bodyClasses = document.body.classList;
console.log("body classes: "+JSON.stringify(bodyClasses));
if (bodyClasses.contains("uninitialized")) {
bodyClasses.remove("uninitialized");
self.port.emit("initialized");
}
document.getElementById("demo").addEventListener("click", function() {
alert("event listener added by content script");
self.port.emit("demo-dun", "demo dun");
});
self.port.on("demo", function(text) {
alert("'demo' event listener called with message '" + text + "'");
});
Use <input type="button" /> to replace the <button> and it will work fine.(or add type="button" to the <button>)
Because in firefox, the default value of <button>'s type is submit.