At the moment I am searching for a method to toggle an alarm for all Users who are looking into the spreadsheet:
// creates a custom menu when the spreadsheet is opened
function onOpen() {
var ui = SpreadsheetApp.getUi()
.createMenu('SWAT Pager')
.addItem('Open SWAT Pager', 'openCallNotifier')
.addToUi();
// you could also open the call notifier sidebar when the spreadsheet opens
// if you find that more convenient
// openCallNotifier();
}
// opens the sidebar app
function openCallNotifier() {
// get the html from the file called "Page.html"
var html = HtmlService.createHtmlOutputFromFile('Page')
.setTitle("SWAT Pager");
// open the sidebar
SpreadsheetApp.getUi()
.showSidebar(html);
}
// returns a list of values in column H
function getColumnE() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Operationen");
// get the values in column H and turn the rows into a single values
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
}
Thats the code.gs
Now what I would like to do is to set a dropdown menu where i can choose:
Warning / No Warning.
When i choose 'warning' I want an alarm sound playing, and on 'no warning' no sound.
Ive tried to change this script into it:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Keine Alarmierung </p>
<audio id="Alarmierung">
<source src="http://banhammer.bplaced.net/audio/spaceship_alarm.mp3" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<script>
var lastTime = []; // store the last result to track changes
function checkCalls() {
// This calls the "getColumnE" function on the server
// Then it waits for the results
// When it gets the results back from the server,
// it calls the callback function passed into withSuccessHandler
google.script.run.withSuccessHandler(function (columnE) {
for (var i = 0; i < columnE.length; i++) {
// if there's a difference and it's a call, notify the user
if (lastTime[i] !== columnE[i] && columnE[i] === "Alarmierung") {
notify();
}
}
// store results for next time
lastTime = columnE;
console.log(lastTime);
// poll again in x miliseconds
var x = 1000; // 1 second
window.setTimeout(checkCalls, x);
}).getColumnE();
}
function notify() {
document.getElementById("Alarmierung").play();
}
window.onload = function () {
checkCalls();
}
</script>
</body>
</html>
But unfortunately it would not work. I would appreciate any suggestions from the community.
Proposed Solution
Your code was generally in the right direction, but it seemed to be doing some necessarily complicated things like:
return sheet.getRange(1, 8, sheet.getLastRow(), 1).getValues().map(function (row) { return row[0]; });
To get a value from E2, which can be done like this:
return sheet.getRange("E2").getValue()
Though maybe you mean to aggregate the values like this for some reason, though it seemed to me that you were tying to get too much working too quickly and were getting confused with what goes where.
In any case, I took your code and simplified it to make an alarm work, which is what I believe your question was really directed towards:
Code.gs
// This function creates a pop up called "Alarm"
function alarm(){
var html = HtmlService.createHtmlOutputFromFile('Page')
.setWidth(400)
.setHeight(300);
SpreadsheetApp.getUi()
.showModalDialog(html, "Alarm");
}
// This is the function called by the client-side js in "Page.html"
// It returns a true or false value depending on the contents of E2
function checkAlarm() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Operationen");
return sheet.getRange("E2").getValue() == "ALARM"
}
Page.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p id="message">Keine Alarmierung </p>
<script>
// This is the call to the checkAlarm function in Code.gs.
// It returns a true or false value which is passes into the function
// playAlarm
google.script.run.withSuccessHandler(playAlarm).checkAlarm();
function playAlarm(bool){
// If the value returned from "checkAlarm" is true
// It created the HTML audio element and makes it play.
if (bool == true){
let audio = document.createElement("audio");
audio.setAttribute("controls", "controls");
audio.setAttribute("autoplay", "autoplay"); // So it plays automatically
audio.setAttribute("style", "display:none"); // So the player is hidden
let source = document.createElement("source");
source.setAttribute("src","https://onlineclock.net/audio/options/police-car.mp3");
source.setAttribute("type","audio/mpeg");
audio.appendChild(source);
document.appendChild(audio);
}
}
</script>
</body>
</html>
Steps to get working.
Paste in the code.
Make sure it is not called onOpen - for an onOpen function to have access to the UI, it needs to be installed not a simple trigger. So go to the triggers in your project and install an onOpen one using the alarm function.
Make sure the value of E2 in Operationen is ALARM.
Refresh the page and the alarm should play!
Notes
Your sound source http://banhammer.bplaced.net/audio/spaceship_alarm.mp3 unfortunately does not work within Apps Script. I believe this is because it is an http origin and not a https origin. This is why I replaced this with another sound https://onlineclock.net/audio/options/police-car.mp3 to get it working.
The script only checks for a match with ALARM in cell E2 in Operationen. Any other value will result in no alarm.
References
ModalDialog
Installable Triggers
Client to Server Communication
Related
I am building a Workspace add-on (in contrast to an Editor add-on). I have successfully created a modal dialog box using the following code:
var html = HtmlService.createHtmlOutputFromFile('dialog.html')
.setWidth(600)
.setHeight(425)
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
SpreadsheetApp.getUi().showModalDialog(html, 'Some title here...');
I can successfully call an Apps Script (server side) function from the dialog.html JavaScript using this syntax:
google.script.run.withFailureHandler(showError).withSuccessHandler(closeThisWindow).onAppsScriptFunction(p1, p2);
On the Apps Script side, I have the function that receives the arguments (p1, p2) from the JavaScript function:
function onAppsScriptFunction(p1, p2) {
console.log(p1 + p2);
// the following does not work
// it simply does nothing
return CardService.newCardBuilder()
.addSection(
CardService.newCardSection().addWidget(
CardService.newTextParagraph().setText("This never appears")
)
)
.build();
}
Everything works as expected, except -- the card is not updated/refreshed. I have digged through all the Apps Script documentation and I don't see how can I refresh the sidebar or card.
Is there a way to initiate a refresh / redraw / restart / anything from the client-side JavaScript. I also tried various combinations with CardService.newActionResponseBuilder() and setStateChanged(true) and setNavigation but nothing works.
Or if that's not possible, at least somehow visually act after a user closes the Modal HTML dialog?
You can only update the card from a context of an Addon trigger
In your case onAppsScriptFunction is not linked to the Addon and thus cannot update the content of the Addon sidebar.
A workaround would be to deploy your modal dialog as a WebApp.
In this case, you can call the WebApp with setOpenLink
You can prompt the Add-on to reload when the WebApp is closed with setOnClose(CardService.OnClose.RELOAD_ADD_ON))
In combination with script properties you can write something like:
Code.gs
function onAppsScriptFunction(p1, p2) {
console.log(p1 + p2);
var props = PropertiesService.getScriptProperties();
props.setProperty("update", "yes");
}
function onHomepage() {
var props = PropertiesService.getScriptProperties();
var update = props.getProperty("update");
if(update == null || update == "no"){
var url =ScriptApp.getService().getUrl();
var button = CardService.newTextButton()
.setText("Open modal dialog as WebApp")
.setOpenLink(CardService.newOpenLink()
.setUrl(url)
.setOnClose(CardService.OnClose.RELOAD_ADD_ON));
var buttonSet = CardService.newButtonSet().addButton(button)
return CardService.newCardBuilder()
.addSection(CardService.newCardSection().addWidget(buttonSet))
.build();
}
else{
props.setProperty("update", "no");
return CardService.newCardBuilder()
.addSection(CardService.newCardSection()
.addWidget(CardService.newTextParagraph().setText("This now appears")))
.build();
}
}
function doGet(){
var html = HtmlService.createHtmlOutputFromFile('dialog.html')
.setWidth(600)
.setHeight(425);
return html;
}
dialog.html:
<html>
<script>
google.script.run.withSuccessHandler(success).onAppsScriptFunction(100, 150);
function success() {
window.top.close();
}
</script>
</html>
After deploying your code as a WEbApp this sample snippet will update your Addon sidebar as desired after the opened window is closed again.
Please mind that depending on your use case you need to decide how to deploy the WebApp and which of the PropertiesServices is the most suitbale for you (script, user or document properties).
I am trying to implement a workflow - I need to send an email with links to approve/reject a candidate to Level 1 Manager and then to Level 2 Manager. Once both approve, a confirmation email is sent to the candidate.
I have a custom function say main_function() that executes before sending each of the two emails. This function needs to pull data from the spreadsheet to which the script is bound.
Since I have a two-step approval, I created different projects to get separate WebApp URLs for each approval step.
I am including the main_function() function as a library in the two projects.
main_function() sends an email with approve/reject link and when the link is clicked an HTML opens with an input box to take comments.
Then the HTML includes a call to a script function saveToSheets() to save the data to google sheet.
The HTML shows up but data is not getting saved because saveToSheets() is not called. How can I resolve this?
Main function in Library myLib
main_function(){
//do something
Logger.log("function called!");
var htmlTemplate = HtmlService.createTemplateFromFile('Index2.html');
htmlTemplate.ID = ID; //pass variables from script to HTML
htmlTemplate.decision = decision;
htmlTemplate= htmlTemplate.evaluate().setTitle('Comments').setSandboxMode(HtmlService.SandboxMode.NATIVE);
return htmlTemplate.asTemplate();
}
saveToSheets(inputArray){
//do something
}
Index2.html in Library myLib
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
</style>
</head>
<body>
// Includes a COMMENT BOX with id comment1
//include DIV element with id output to catch error
<script>
function runGoogleScript() {
var item0 = "<?= ID ?>";
var item1 = "<?= decision ?>";
var item2 = document.getElementById('comment1').value; //comments
var inputArray =[item0,item1,item2];
google.script.run.withFailureHandler(onFailure).myLib.saveToSheets(inputArray);
}
function onFailure(error) {
var div = document.getElementById('output');
div.innerHTML = "ERROR: " + error.message;
}
function onSuccess() {
}
</script>
</body>
</html>
Function in another project that needs to reuse the script and HTML through mylib
myfunction(){
var htmlTemplate = mylib.main_function();
return htmlTemplate.evaluate();
}
It is incorrect to say "Index.html is not accessible from project". When you deploy a project as a library, its HTML files are in the context of the library.
But if you want to pass an evaluated HTML template from a library as a template you should use asTemplate()
Example:
main_function(){
//do something
Logger.log("function called!");
var htmlTemplate = HtmlService.createTemplateFromFile('Index.html');
htmlTemplate= htmlTemplate.evaluate();
return htmlTemplate.asTemplate();
}
To facilitate the annotation of audio files in a Google spreadsheet, I'd like to implement an audio player in the sidebar which automatically plays the audio file mentioned as URL in the row of a table. After listening and entering some date in this row, I'd like to move to the next row and do the same. Thus, the URL to the audio file should be updated whenever I select a new row and the whole process should be fast, too, in order to listen quickly to one sound file after the other.
I've experimented with the solution mentioned in this SO post, but this solution is relying on a poll function with a time interval, which is impractical for me as it periodically is updating the sidebar. Crucial for me would be to update the content of the sidebar only once.
Code.gs
var SIDEBAR_TITLE = 'Opnam lauschteren';
/**
* Adds a custom menu with items to show the sidebar and dialog.
*
* #param {Object} e The event parameter for a simple onOpen trigger.
*/
function onOpen(e) {
SpreadsheetApp.getUi()
.createAddonMenu()
.addItem('Opname lauschteren', 'showSidebar')
.addToUi();
}
/**
* Runs when the add-on is installed; calls onOpen() to ensure menu creation and
* any other initializion work is done immediately.
*
* #param {Object} e The event parameter for a simple onInstall trigger.
*/
function onInstall(e) {
onOpen(e);
}
/**
* Opens a sidebar. The sidebar structure is described in the Sidebar.html
* project file.
*/
function showSidebar() {
var ui = HtmlService.createTemplateFromFile('Sidebar')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle(SIDEBAR_TITLE);
SpreadsheetApp.getUi().showSidebar(ui);
}
function getValues() {
var app = SpreadsheetApp;
var value = app.getActiveSpreadsheet().getActiveSheet().getActiveCell().getValue();
Logger.log(value);
return value;
}
function getRecord() {
// Retrieve and return the information requested by the sidebar.
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var headers = data[0];
var rowNum = sheet.getActiveCell().getRow();
if (rowNum > data.length) return [];
var record = [];
for (var col=0;col<headers.length;col++) {
var cellval = data[rowNum-1][col];
// Dates must be passed as strings - use a fixed format for now
if (typeof cellval == "object") {
cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
}
// TODO: Format all cell values using SheetConverter library
record.push({ heading: headers[col],cellval:cellval });
}
Logger.log(record);
return record;
}
Sidebar.html
<!-- Use a templated HTML printing scriptlet to import common stylesheet. -->
<?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>
<!-- Below is the HTML code that defines the sidebar element structure. -->
<div class="sidebar branding-below">
<!-- The div-table class is used to make a group of divs behave like a table. -->
<div class="block div-table" id="sidebar-record-block">
</div>
<div class="block" id="sidebar-button-bar">
</div>
<div id="sidebar-status"></div>
<!-- Use a templated HTML printing scriptlet to import JavaScript. -->
<?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>
</div>
<!-- Enter sidebar bottom-branding below. -->
<div class="sidebar bottom">
<span class="gray branding-text">PG</span>
</div>
SidebarJavaScript.html
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
/**
* Run initializations on sidebar load.
*/
$(function() {
// Assign handler functions to sidebar elements here, if needed.
// Call the server here to retrieve any information needed to build
// the dialog, if necessary.
// Start polling for updates
poll();
});
/**
* Poll a server-side function at the given interval, to have
* results passed to a successHandler callback.
*
* https://stackoverflow.com/a/24773178/1677912
*
* #param {Number} interval (optional) Time in ms between polls.
* Default is 2s (2000ms)
*/
function poll(interval) {
interval = interval || 3000;
setTimeout(function() {
google.script.run
.withSuccessHandler(showRecord)
.withFailureHandler(
function(msg, element) {
showStatus(msg, $('#button-bar'));
element.disabled = false;
})
.getRecord();
}, interval);
};
/**
* Callback function to display a "record", or row of the spreadsheet.
*
* #param {object[]} Array of field headings & cell values
*/
function showRecord(record) {
if (record.length) {
for (var i = 2; i <= 2; i++) {
// build field name on the fly, formatted field-1234
var str = '' + i;
var fieldId = 'field-' + ('0000' + str).substring(str.length)
// If this field # doesn't already exist on the page, create it
if (!$('#'+fieldId).length) {
var newField = $($.parseHTML('<div id="'+fieldId+'"></div>'));
$('#sidebar-record-block').append(newField);
}
// Replace content of the field div with new record
$('#'+fieldId).replaceWith('<div id="'+fieldId+'" class="div-table-row"></div>');
$('#'+fieldId).append($('<div class="div-table-th">' + record[i].heading + '</div>'))
.append('<audio id="player" controls > <source src=' + record[i].cellval + ' type=audio/wav > Your browser does not support the audio element. </audio>');
}
}
// TODO: hide any existing fields that are beyond the current record length
//Setup the next poll
poll();
}
/**
* Displays the given status message in the sidebar.
*
* #param {String} msg The status message to display.
* #param {String} classId The message type (class id) that the message
* should be displayed as.
*/
function showStatus(msg, classId) {
$('#sidebar-status').removeClass().html(msg);
if (classId) {
$('#sidebar-status').addClass(classId);
}
}
</script>
A reproducible example is accessible here; Add-ons > 'play audio' (Google account necessary).
I am struggling finding a method to trigger the update of the sidebar only once and only when a new row is selected. The use of a sidebar is not mandatory, rather another solution, e.g. with a automatically updated 'Play' button, would be helpful, too.
I made some small changes to the example code you provided so that the sidebar does not update periodically following the time interval.
Basically, I've used PropertiesService to store the row that is selected. The idea is that the script checks whether the currently selected row and the previously selected row (the one selected last time getRecord was called, that is, during last interval) are the same. If they are the same, there hasn't been a row selection change, which means the audio in the sidebar doesn't need updating.
So it only updates if the selected row changes, which is, I think, the main issue you are having.
To achieve this, your code would have to be modified in the following way (look at inline comments for details on the changes):
getRecord()
function getRecord() {
var scriptProperties = PropertiesService.getScriptProperties();
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var headers = data[0];
var rowNum = sheet.getActiveCell().getRow(); // Get currently selected row
var oldRowNum = scriptProperties.getProperty("selectedRow"); // Get previously selected row
if(rowNum == oldRowNum) { // Check if the was a row selection change
// Function returns the string "unchanged"
return "unchanged";
}
scriptProperties.setProperty("selectedRow", rowNum); // Update row index
if (rowNum > data.length) return [];
var record = [];
for (var col=0;col<headers.length;col++) {
var cellval = data[rowNum-1][col];
if (typeof cellval == "object") {
cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
}
record.push({ heading: headers[col],cellval:cellval });
}
return record;
}
Depending on whether there was a selection change, getRecord returns:
a record array, if the selected row is different.
the string "unchanged", if the selected row is the same. Probably this is not the most elegant way to handle this, but you get the idea.
Then, showRecord(record) gets this returned value. If this value is the string "unchanged", it won't update the sidebar:
showRecord(record)
function showRecord(record) {
// Checks whether returned value is `"unchanged"` (this means the row selected is the same one as before)
if (record != "unchanged" && record.length) {
for (var i = 2; i <= 2; i++) {
// build field name on the fly, formatted field-1234
var str = '' + i;
var fieldId = 'field-' + ('0000' + str).substring(str.length)
// If this field # doesn't already exist on the page, create it
if (!$('#'+fieldId).length) {
var newField = $($.parseHTML('<div id="'+fieldId+'"></div>'));
$('#sidebar-record-block').append(newField);
}
// Replace content of the field div with new record
$('#'+fieldId).replaceWith('<div id="'+fieldId+'" class="div-table-row"></div>');
$('#'+fieldId).append($('<div class="div-table-th">' + record[i].heading + '</div>'))
.append('<audio id="player" controls autoplay> <source src=' + record[i].cellval + ' type=audio/wav > Your browser does not support the audio element. </audio>');
}
}
// TODO: hide any existing fields that are beyond the current record length
//Setup the next poll
poll();
}
I also added the autoplay attribute in this line:
.append('<audio id="player" controls> <source src=' + record[i].cellval + ' type=audio/wav > Your browser does not support the audio element. </audio>')
So that the audio plays automatically when you select a new row, without having to click the play button.
Finally, I changed the poll interval to 500, so that you don't have to wait so much for the new audio to play. Anyway you can edit this to whatever suits you best:
interval = interval || 500;
I didn't modify the rest of the script, even though it can probably be improved owing to the fact that it was mainly written for a different issue.
I hope this is of any help.
Playing My Music
I added a play this button to each of my playlist selections. Perhaps this will help you to accomplish what you wish.
code.gs:
function onOpen() {
SpreadsheetApp.getUi().createMenu('My Music')
.addItem('Launch Music', 'launchMusicDialog')
.addToUi();
}
function convMediaToDataUri(filename){
var filename=filename || "You Make Loving Fun.mp3";//this was my debug song
var folder=DriveApp.getFolderById("Music Folder Id");
var files=folder.getFilesByName(filename);
var n=0;
while(files.hasNext()) {
var file=files.next();
n++;
}
if(n==1) {
var blob=file.getBlob();
var b64DataUri='data:' + blob.getContentType() + ';base64,' + Utilities.base64Encode(blob.getBytes());
Logger.log(b64DataUri)
var fObj={filename:file.getName(),uri:b64DataUri}
return fObj;
}
throw("Multiple Files with same name.");
return null;
}
function launchMusicDialog() {
var userInterface=HtmlService.createHtmlOutputFromFile('music1');
SpreadsheetApp.getUi().showModelessDialog(userInterface, 'Music');
}
function doGet() {
return HtmlService.createHtmlOutputFromFile('music1').addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function getPlaylist() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('MusicList');
var rg=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn());
var vA=rg.getValues();
var pl=[];
var idx=0;
var html='<style>th,td{border:1px solid black;}</style><table><tr><th>Index</th><th>Item</th><th>FileName</th><th> </th></tr>';
for(var i=0;i<vA.length;i++) {
if(vA[i][4]) {
pl.push(vA[i][1]);
html+=Utilities.formatString('<tr><td>%s</td><td>%s</td><td>%s</td><td><input type="button" value="Play This" onClick="playThis(%s)" /></td></tr>',idx,vA[i][0],vA[i][1],idx++);
}
}
html+='</table>';
return {playlist:pl,html:html};
}
music1.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<style>
label{margin:2px 10px;}
</style>
</head>
<script>
var selectionList=["BarbaraAnn.mp3","Don't Let Me Come Home a Stranger.mp3"];
var gVolume=0.2;
var index=0;
$(function(){
document.getElementById('msg').innerHTML="Loading Playlist";
google.script.run
.withSuccessHandler(function(Obj){
selectionList=Obj.playlist;
console.log(Obj.playlist);
document.getElementById('list').innerHTML=Obj.html;
google.script.run
.withSuccessHandler(function(fObj){
$('#audio1').attr('src',fObj.uri);
var audio=document.getElementById("audio1");
audio.volume=gVolume;
audio.onended=function() {
document.getElementById('status').innerHTML='Ended...';
playnext();
}
var msg=document.getElementById('msg');
msg.innerHTML="Click play to begin playlist. Additional selections will begin automatically";
audio.onplay=function() {
document.getElementById('msg').innerHTML='Playing: ' + selectionList[index-1];
document.getElementById('status').innerHTML='Playing...';
document.getElementById('skipbtn').disabled=false;
}
audio.onvolumechange=function(){
gVolume=audio.volume;
}
})
.convMediaToDataUri(selectionList[index++]);
})
.getPlaylist();
});
function playnext() {
if(index<selectionList.length) {
document.getElementById('status').innerHTML='Loading...';
document.getElementById('msg').innerHTML='Next Selection: ' + selectionList[index];
google.script.run
.withSuccessHandler(function(fObj){
$('#audio1').attr('src',fObj.uri);
var audio=document.getElementById('audio1');
audio.volume=gVolume;
audio.play();
})
.convMediaToDataUri(selectionList[index++]);
}else{
document.getElementById('status').innerHTML='Playlist Complete';
document.getElementById('msg').innerHTML='';
document.getElementById('cntrls').innerHTML='<input type="button" value="Replay Playlist" onClick="replayPlaylist()" />';
}
}
function replayPlaylist() {
index=0;
document.getElementById('cntrls').innerHTML='';
playnext();
}
function skip() {
var audio=document.getElementById('audio1');
document.getElementById('skipbtn').disabled=true;
audio.pause();
playnext();
}
function playThis(idx) {
index=idx;
var audio=document.getElementById('audio1');
//audio.pause();
playnext();
}
</script>
<body>
<div id="msg"></div>
<audio controls id="audio1" src=""></audio><br />
<div id="status"></div>
<div><input type="button" id="skipbtn" value="Skip" onClick="skip()" disabled /></div>
<div id="cntrls"></div>
<div id="list"></div>
</body>
</html>
Admittedly, the transition is a little rough but I didn't put that much effort into the modification so perhaps you can smooth it out a little. Just run launchMusicDiaog() to get it going. There's also a doGet() in there for the webapp.
I wrote a google apps script code, it will open a google spread sheet, and list the values by row, but there are 2 problems:
1. The output by random order.
2. The div text which id "loding" change to "Finished!" before list all of values.
I thought the script will wait for server-side function return when I run it by "withSuccessHandler()", but it's not.
How can I correct it?
index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function jsListValue() {
// Get count.
google.script.run.withSuccessHandler(function(count) {
// List all values.
for( count; count>0; count=count-1) {
// Get a value.
google.script.run.withSuccessHandler(function(content) {
// Shows in "output".
var new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(content));
document.getElementById("output").appendChild(new_div);
}).gsGetValue(count);
}
// Change loding notice.
document.getElementById("loding").innerHTML = "Finished!";
}).gsGetCount();
}
</script>
</head>
<body onload="jsListValue()">
<div id="output"></div>
<div id="loding">Loding now...</div>
</body>
</html>
code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function gsOpenSheet() {
// Return sheet of the note data.
return (SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1"));
}
function gsGetCount() {
// Return last row index in this sheet.
return (gsOpenSheet().getLastRow());
}
function gsGetValue(index) {
// Return value in the (index,1).
return (gsOpenSheet().getRange(index,1).getValue());
}
GAS is very similar to Javascript, and all calls to Google's server side functions are asynchronous. You cannot change this (at least I haven't seen any doc reg. that).
What you can do, is, use a callback function on the client side which polls the server for a "success" return value. It'll keep polling it say for 1 minute, or else exit. Let it set a client flag to "true" if the success value is returned by the server. Nothing should proceed on the client side, unless the flag is true. In this way, you can control what happens on the client side.
You want to use withSuccessHandler Docs
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function onSuccess(numUnread) {
var div = document.getElementById('output');
div.innerHTML = 'You have ' + numUnread
+ ' unread messages in your Gmail inbox.';
}
google.script.run.withSuccessHandler(onSuccess)
.getUnreadEmails();
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>
google.script.run is asynchronous, which means that is impossible to predict in what order gsGetValue(count) will return. When you're using withSuccessHandler you must perform the next action inside the callback function.
My suggestion is to get all the range you want and put it on an array. You can create a serverside function to do this. The code would look like this:
//Serverside function
function getDataForSearch() {
const ws = SpreadsheetApp.openById("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getSheetByName("sheet1");
return ws.getRange(1,1,ws.getLastRow(),1).getValues();
}
getValues() will return an array of arrays that should contain all values from the range specified. More information about here
On your client-side, the script should be like this:
//array to get the data from getRange()
var data;
function jsListValue() {
google.script.run.withSuccessHandler(function(dataReturned){
data = dataReturned;
var new_div;
data.forEach(function(r){
new_div = document.createElement("div");
new_div.appendChild(document.createTextNode(r[0));
document.getElementById("output").appendChild(new_div);
});
document.getElementById("loding").innerHTML = "Finished!";
}).getDataForSearch();
}
As hinted by #Iuri Pereira If you return a parameter from the google script code, and then use it in the page javascript, then the procedure runs synchronously.
HTML:
<button onclick="google.script.run.withSuccessHandler(js_function).gs_code();">action</button>
GS:
function gs_code() {
// do Something;
return true;
}
HTML javascript:
function js_function(gs_return_value) {
console.log(gs_return_value);
}
So I'm basically developing an automated click-paste-and-upload system for mutiple texts and files inside a google page.
This method helped me get the instances of objects that I'm looking for: buttons, textboxes, richtextboxes, etc.
Now I want to work with them.
So for example I know the id of a button , and the function subscribed to its click event. How do I trigger the click event from the extension ? I've tried injecting a script with the click event handler (discovered with DOM inspector) at "document_startup" but I don't get an error or anything else.
Here's the content script! The loggerhead function should have inserted the script but I don't think it did. What might be the reason for the blow code not giving anything?
// Runs a function for every added DOM element that matches a filter
// filter -- either function(DOM_node){/*...*/}, returns true or false
// OR a jQuery selector
// callback -- function(DOM_node){/*...*/}
function watchNodes(filter, callback){
observer = new MutationObserver( function (mutations) {
mutations.forEach( function (mutation){
if(typeof filter === "function"){
$(mutation.addedNodes).filter(
function(i){ return filter(this); }
).each(
function(i){ callback(this); }
);
} else {
$(mutation.addedNodes).filter(filter).each(
function(i){ callback(this); }
);
}
});
});
// For every added element, a mutation will be processed
// with mutation.taget == parent
// and mutation.addedNodes containing the added element
observer.observe(document, { subtree: true, childList: true });
}
function loggerhead(node) {
console.log("passhead");
//also inject jquery
var jqueryEl = document.createElement('script');
jqueryEl.setAttribute('src', chrome.extension.getURL('jquery-1.11.1.min.js'));
jqueryEl.setAttribute('type', 'text/javascript');
var scriptEl = document.createElement('script');
scriptEl.setAttribute('src', chrome.extension.getURL('script.js'));
scriptEl.setAttribute('type', 'text/javascript');
node.appendChild(jqueryEl);
node.appendChild(scriptEl);
}
watchNodes("head", loggerhead);
// method not working
//var gmailHead = jQuery("head", document).get(0);
script.js contains the function of subscribed to the click event of the button that I've managed to find through the DOM inspector:
function Cdb(b){return function(){if(Vbb()){return Ddb(b,this,arguments)}else{var a=Ddb(b,this,arguments);a!=null&&(a=a.val);return a}}}
You should try to call the existing click handler like
buttonElement.click()