Google Sheets Index Sidebar of all tabs - add search functionality - javascript

After some searching I found this code to get a sidebar of all tabs (workbooks) in Google Sheets as a hyperlink. I am looking to add two functionalities to this, if its possible
Search function - a search box on sidebar to look up tabs more easily
Same tab - open the clicked hyperlinked tab in the same browser tab instead of opening in a new window.
Here's the code so far:
function onOpen() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Sidebar Menu')
.addItem('Show sidebar', 'showSidebar')
.addToUi();
}
function showSidebar() {
var ui = HtmlService.createTemplateFromFile('sidebar.html')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Index Sidebar');
SpreadsheetApp.getUi().showSidebar(ui);
}
function getSheetNames() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
return sheetNamesIds(sheets);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexOfSheets = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexOfSheets.push([sheet.getSheetName(),sheet.getSheetId()]);
});
//Logger.log(indexOfSheets);
return indexOfSheets;
}
HTML
<!DOCTYPE html>
<h1>Index of all sheets in this workbook:</h1>
<input type="button" value="Close" onclick="google.script.host.close()" />
<ol>
<?!= getSheetNames().map(function(d) {
return "<li><a href='https://docs.google.com/spreadsheets/d/1234/edit#gid=" + d[1] + "' target='_blank'>" + d[0] + "</a></li>";
}).join(''); ?>
</ol>

Proposed Solution
You should be able to replace the code you posted with what is below and have it work. First copy and paste it into the two files and then run it from the Sheet, not from the Script Editor.
HTML
<!DOCTYPE html>
<h1>Index of all sheets in this workbook:</h1>
<script>
function removeElement(elementId) {
var element = document.getElementById(elementId);
element.parentNode.removeChild(element);
}
function buildList(text) {
google.script.run.withSuccessHandler(onSuccess).returnListItems(text)
}
function onSuccess(result) {
var element = document.createElement("ol")
element.innerHTML = result
var sidebar = document.getElementById("sidebar")
sidebar.appendChild(element)
}
function getTextAndSearch() {
var text = document.getElementById("text-search").value
removeElement("ol")
buildList(text)
}
</script>
<sidebar id="sidebar">
<input type="button" value="Close" onclick="google.script.host.close()" />
<br>
<input type="text" id="text-search" />
<input type="button" value="Search" onclick="getTextAndSearch()" />
<ol id="ol">
<?!=
returnListItems()
?>
</ol>
</sidebar>
JS
function onOpen() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Sidebar Menu')
.addItem('Show sidebar', 'showSidebar')
.addToUi();
}
function showSidebar() {
var ui = HtmlService.createTemplateFromFile('sidebar.html')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Index Sidebar');
SpreadsheetApp.getUi().showSidebar(ui);
}
function getSheetNames() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
return sheetNamesIds(sheets);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexOfSheets = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexOfSheets.push([sheet.getSheetName(),sheet.getSheetId()]);
});
//Logger.log(indexOfSheets);
return indexOfSheets;
}
// function to return a button with onclick attribute for each sheet that matches
function returnListItems(text) {
var sheetNames = getSheetNames()
// Checking if there is a search term
if (text) {
sheetNames = sheetNames.filter(n => n[0].includes(text))
}
var htmlString = sheetNames.map(function(d) {
var string = `
<li>
<input
type="button"
value="${d[0]}"
onclick=\"google.script.run.setActiveByName('${d[0]}')\"/>
</li>
`
return string }).join(' ')
return htmlString
}
// Utility function to set Active sheet by name.
function setActiveByName(name) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
SpreadsheetApp.setActiveSheet(ss)
}
You may need to create a dummy function in the script editor: function init(){} and run it just to grant the permissions the script needs, though this should happen when you run the script from the menu.
Explanation
Bringing sheets into focus - setActiveSheet
This is relatively simple so I have included it in the answer, though technically is a second question. In future ensure to only ask one question per post, it keeps things tidier on the site and easier for future users to search for answers. Thank you
This involves using setActiveSheet for which I made the utility function setActiveByName.
To insert this function into each link, I wrote the function returnListItems, that generates and returns the HTML for each button to call the setActiveByName when clicked. Writing it as a second function was not strictly necessary, but it made things clearer, especially since it would make the main question a lot easier.
Search function
What seemed relatively simple when I started quickly got quite complicated. It involved breaking up some of the code into its component functionality, to make it easier to work with. Yet the biggest challenge is properly setting up the server side code (Apps Script editor) and the client side code (contained in the HTML).
Once the Sidebar is loaded, I found that any further manipulation of the sidebar HTML needed to be done client side, hence the functions in the HTML. The solution lay in the "Client-to-Server Communication" article in the docs.
The search button is pressed, then the client side script gets the text in the Search input box, passes it to the buildList function, also client side, which in turn calls:
google.script.run.withSuccessHandler(onSuccess).returnListItems([SEARCH_TERM])
This is a asynchronous function that goes server side. The "Success Handler" is the call back function that runs when the list items are returned. onSuccess simply updates the HTML in the sidebar. I modified the returnListItems function to be able to accept a search term, so when it runs the first time, it runs without any arguments so all sheets are returned. When called by the client side function, it runs with a text. If the sheet name contains the search term, it is listed.
References
Spreadsheet Service
Create and Serve HTML
HtmlService
Client-Server Communications
JS includes (for search)

Thank you Ian for your solution above. Works well! After some trial and error, I was able to tweak it to ensure hidden sheets aren't included. Below is the updated code for anyone else interested:
function onOpen() {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Sidebar Menu')
.addItem('Show sidebar', 'showSidebar')
.addToUi();
}
function showSidebar() {
var ui = HtmlService.createTemplateFromFile('Sidebar.html')
.evaluate()
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Index Sidebar');
SpreadsheetApp.getUi().showSidebar(ui);
}
function getSheetNames() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets().filter(s => !s.isSheetHidden());
return sheetNamesIds(sheets);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexOfSheets = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexOfSheets.push([sheet.getSheetName(),sheet.getSheetId()]);
});
//Logger.log(indexOfSheets);
return indexOfSheets;
}
// function to return a button with onclick attribute for each sheet that matches
function returnListItems(text) {
var sheetNames = getSheetNames()
// Checking if there is a search term
if (text) {
sheetNames = sheetNames.filter(n => n[0].includes(text))
}
var htmlString = sheetNames.map(function(d) {
var string = `
<li>
<input
type="button"
value="${d[0]}"
onclick=\"google.script.run.setActiveByName('${d[0]}')\"/>
</li>
`
return string }).join(' ')
return htmlString
}
// Utility function to set Active sheet by name.
function setActiveByName(name) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
SpreadsheetApp.setActiveSheet(ss)
}

Related

Move sheet when drop-down menu is selected

I want to implement a function that moves to the selected sheet when selecting the drop-down menu (sheet name) in Google spreadsheet, but it's not working well.
The menu shows all the seat names, but if you select that particular seat, nothing will happen.
I keep searching and searching, but I don't see a clear solution. What should I do? Please help me.
The code I wrote is as follows.
code.gs
function onOpen() {
createMenu()
}
function createMenu(){
const ui = SpreadsheetApp.getUi()
const menu = ui.createMenu("Sidebar")
menu.addItem("Open Sidebar", "openSidebar");
menu.addToUi()
}
function openSidebar() {
ss = SpreadsheetApp.getActiveSpreadsheet();
var html = HtmlService.createTemplateFromFile('sidebar')
.evaluate()
.setTitle('Salesforce')
.setWidth(400);
SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.
.showSidebar(html);
}
sidebar.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<button id = "moveButton" class="button alert">Move</button>
<select name="Available sheets" onchange="moveSheet()" style="width:280px;height:30px;">
<? var sheets=ss.getSheets(); ?>
<? for(var i=0;i<sheets.length;i++) { ?>
<option value=<?=sheets[i].getName()?>> <?= sheets[i].getName()?></option>
<? } ?>
</select>
<script>
function myJsFunction(){
var name = document.getElementsByName("Available sheets")[0].value;
}
function moveSheet(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(name);
if(sheet !=null){
ss.moveActiveSheet(sheet);
}else{
//
}
}
document.getElementById("moveButton").addEventListener("click",moveSheet());
//google.script.run.withSuccessHandler(name);
};
</script>
</body>
</html>
I believe your goal is as follows.
You want to move the sheet by selecting the sheet name from the dropdown list at the sidebar of Spreadsheet.
When I saw your showing script, I thought that you are trying to run the Google Apps Script at the sidebar. At the sidebar and dialog of Spreadsheet, the Javascript is run with the client browser. On the other hand, Google Apps Script is run on the internal server side of Google. I thought that this might be the reason for your issue.
When your showing script is modified for achieving your goal, how about the following modification?
Google Apps Script side: code.gs
In this modification, your function of openSidebar is modified and a new function of moveSheet is added.
Here, in order to move the active sheet, activate() is used.
function moveSheet(e) {
SpreadsheetApp.getActiveSpreadsheet().getSheetByName(e).activate();
}
function openSidebar() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var options = ss.getSheets().map(s => {
var sheetName = s.getSheetName();
return `<option value="${sheetName}">${sheetName}</option>`;
}).join("");
var html = HtmlService.createTemplateFromFile('sidebar');
html.options = options;
var h = html.evaluate().setTitle('Salesforce').setWidth(400);
SpreadsheetApp.getUi().showSidebar(h);
}
HTML side: sidebar.html
I modified select tag and removed moveSheet().
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<button id="moveButton" class="button alert">Move</button>
<select name="Available sheets" onchange="google.script.run.moveSheet(this.value)" style="width:280px;height:30px;">
<?!= options ?>
</select>
<script>
document.getElementById("moveButton").addEventListener("click",moveSheet());
//google.script.run.withSuccessHandler(name);
};
</script>
</body>
</html>
When you run openSidebar, a sidebar is opened. And, you can see the dropdown list including the sheet names. When you change the dropdown list, the active sheet is changed to the sheet of the selected sheet name.
References:
HTML Service: Templated HTML
activate()

how to append html page inside current page using google web app?

I know how to include CSS or JS files using app script in a web script. but that way includes files content on page lode.
My question is it possible to include partial html page inside the currently opened page?
app script to include CSS or JS
/* #Include JavaScript & CSS & HTML-Partial-Views */
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
and I use like this
<?!= include('Css'); ?>
and here is my attempt.
html
<button onclick="getList("users")">show some html content</button>
<div id="users"></div>
<script>
function getList(users){
var listUsers = google.script.run.showHtml(users);
// how to return showHtml result [list of users]
for (var i; i <= listUsers.length; 1++)
{
document.querySelector("#users").innerHTML += <div>listUsers[i]</div>;
}
}
</script>
gs
function users(sheetName, pageName){
// get users from sheet
var ss= SpreadsheetApp.openById("435yh35h45b35nh6hg5bwh455j");
var dataSheet = ss.getSheetByName(sheetName);
var dataRange = dataSheet.getDataRange().getValues();
return dataRange;
}
From one of the code snippets
// how to return showHtml result [list of users]
Try this:
<button onclick="getList('users')">show some html content</button>
<div id="users"></div>
<script>
function getList(users){
var listUsers = google.script.run
.withSuccessHandler((listUsers) => {
let list = '';
for (var i; i < listUsers.length; i++) {
list += `<div>${listUsers[i]}</div>\n`;
}
document.querySelector("#users").innerHTML = list;
})
.showHtml(users);
}
</script>
Changes done
Replaced "users" by 'users'
Added withSuccessHandler with an arrow function as callback.
The arrow function build a string using a for statement (because it was used in the original code) including a div tag using a template literal.

Google Apps Script - return output from apps script function back to the html file javascript function

Ok, so I am trying to make a function in google sheets that when the user selects a cell and then runs the function (currently trying to make), a sidebar getting all the synonyms of the word should appear. I am using https://words.bighugelabs.com/ to get the synonyms. So first I make the menu:
`function onOpen(e) {
var ui = SpreadsheetApp.getUi().createMenu("Sidebar")
.addItem("Get Synonym", 'showSidebar')
.addToUi();
}`
Then this is the showSidebar function:
function showSidebar() {
var html = HtmlService.createHtmlOutputFromFile("Test")
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setWidth(150)
.setTitle("My Sidebar");
SpreadsheetApp.getUi().showSidebar(html);
}
This is the html file:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
function doSomething() {
var synonyms = google.script.run.getSynonym();
document.getElementById("synonyms").innerHTML = synonyms;
}
</script>
</head>
<body>
<div>
<span style="color:orange;">This is a test sidebar!</span>
<button onclick="doSomething()">Click Me!</button>
<div id="synonyms"></div>
</div>
</body>
</html>
And this is the getSynonym function:
function getSynonym() {
var word = SpreadsheetApp.getActiveRange().getValue();
var synonyms = [];
var response = UrlFetchApp.fetch("http://words.bighugelabs.com/api/2/{my_api_key}/" + word + "/json");
response = JSON.parse(response);
var synonyms = response.adjective.syn;
return synonyms;
}
But the variable synonyms which as an array of synonyms doesn't get returned to the doSomething function in the Html file.
What I want is that the sidebar should get a list of all the synonyms.
So basically I can't get the data from one function to another...and I want to know if this is the right way.
When calling server side functions using google.script.run you need to define a success handler, which will asynchronously receive your response.
See the examples on: https://developers.google.com/apps-script/guides/html/communication
function onSuccess(synonyms ) {
console.log(synonyms);
}
google.script.run.withSuccessHandler(onSuccess).doSomething();

Button Action to retrieve data from spreadsheet using google app script

How to retrieve a complete row from a spreadsheet based on a filter on an action such as a click of a button.
I read that GAS is server-side scripting and it is complex to gain access to a spreadsheet.
Is that so. Please guide me.
I have done till this:
$("#form-action")
.button()
.click(function() {
var ss = SpreadsheetApp.openById("");
var sheet = SpreadsheetApp.getActiveSpreadsheet();
SpreadsheetApp.setActiveSheet(sheet.getSheetByName('Test'));
SpreadsheetApp.getActiveSheet().getRange("D1").setFormula('Query(A:C,"SELECT A,B,C WHERE B="' + "mydata'" + ',1)');
SpreadsheetApp.getActiveSheet().getRange("E:J").getValues();
});
Gaining access to the spreadsheet is not difficult at all. You have to remember that while Google Apps Script runs on Google servers, the client-side code (e.g. HTML and JavaScript code you use in your UI templates) will be sent to your browser for rendering, so you can't really mix the two and write jQuery code in GAS(.gs) files or vice versa.
To clarify, commands like
var ss = SpreadsheetApp.openById("");
must be kept in .gs files. To use client-side HTML and JavaScript, you must create separate HTML files in your project (go to File - New - HTML file). Here's more information on serving HTML in GAS https://developers.google.com/apps-script/guides/html/
Luckily, Google provides the API that allows you to communicate between client and server sides by calling 'google.script.run.' followed by the name of the function in '.gs' file.
Example function in '.gs' file
function addRow() {
var sheet = SpreadsheetApp.getActive()
.getSheets()[0];
sheet.appendRow(['Calling', 'server', 'function']);
}
In your HTML template file, here's how you would call this function
<script>
google.script.run.addRow();
</script>
Consider the example that is more relevant to your situation. In my spreadsheet, the QUERY formula changes dynamically based on the value entered by the user. The form with input field is displayed in the sidebar.
Project structure
Code for 'sidebar.html' is below. Note that using the 'name' attribute of the <input> element is mandatory. On form submit, the value of the attribute ('filterBy') will be transformed into propetry of the form object that we can reference in our server function to get user input.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js">
</script>
</head>
<body>
<form id="myForm">
<input type="text" name="filterBy">
<input type="submit" value="submit">
</form>
<table id="myTable"></table>
<script>
$('document').ready(function(){
var form = $('#myForm');
var table = $('#myTable');
var runner = google.script.run;
form.on('submit', function(event){
event.preventDefault(); //prevents <form> redirecting to another page on submit
table.empty(); // clear the table
runner.withSuccessHandler(function(array){ //this callback function will be invoked after the 'retriveValues()' function below
for (var i = 0; i < array.length; i++) {
var item = '<tr><td>' + array[i] +'</td></tr>';
table.append(item);
}
})
.retrieveValues(this); //the function that will be called first. Here, 'this' refers to the form element
});
});
</script>
</body>
</html>
Code in '.gs' file:
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheets()[0];
function onOpen() {
var ui = SpreadsheetApp.getUi();
var htmlOutput = HtmlService.createTemplateFromFile('sidebar')
.evaluate();
ui.showSidebar(htmlOutput);
}
function retrieveValues(req) {
var res = [];
var filterBy = req.filterBy; //getting the value of the input field.
sheet.getRange(1, 2, 1, 1)
.setFormula("QUERY(A1:A, \"SELECT A WHERE A > " + filterBy + "\")");
sheet.getRange(1, 2, sheet.getLastRow(), 1)
.getValues()
.map(function(value){
if (value[0] != "") res = res.concat(value[0]); // get only the values that are not empty strings.
});
return res;
}
Here's the result of entering the value and submitting the form. The server-side function returns the array of values greater than 5. The callback function that we passed as parameter to 'withSuccessHandler' then receives this array and populates the table in the sidebar.
Finally, I'm not sure why you are using the QUERY formula. Instead of modifying 'SELECT' statement, you could simply take the values from the target range an filter them in GAS.

Google App Script Bounded to Spreadsheet

what I' trying to accomplish is a Google Spreadsheet for a project management. I've got lots of cells in a grid where a user should select either the item was completed or not. Now this spreadsheet would be available only to a Project Manager. The way I imagined the process would work was that Project Manager selects particular cells and assigns them to a technician's email address. Script would then generate mobile friendly html UI and send it to the technician (I thought of Google forms but I want to create more customized UI). Technician would then select a checkbox after completing a task which would at the same time update the spreadsheet. Next time technician would open the UI it would populate all the checkboxes that previously were selected.
The only way I've found that I could make it work was a google script web app bounded to a spreadsheet. I've created a test HTML file and .gs file:
.html file
<head>
<base target="_top">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
<h1> Web App Test </h1>
<input type="button" value="Click Me" id="buttonclicked" onclick="getSomeData()"/>
<div id="output" class="current">output</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script>
function getSomeData()
{
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(showError)
.testForWebApp();
myLog("in WebAppTest.html getSomeData()");
}
function onSuccess(testParam)
{
var div = document.getElementById('output');
if (sectionName == null)
div.innerHTML = "<p style='color:red;'>You didn't hit the script</p>";
else
div.innerHTML = "<p style='color:white;'>" + testParam + "</p>";
}
function showError()
{
var div = document.getElementById('output');
div.innerHTML = "<p style='color:red;'>You didn't hit the script</p>";
}
</script>
</body>
and .gs file:
function doGet()
{
return HtmlService.createHtmlOutputFromFile('WebAppTest')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function testForWebApp()
{
myLog("In testForWebApp()");
var msg = "Yep you hit the script!";
return msg;
}
function myLog(log)
{
//log = 'test';
Logger.log(log);
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName('log');
var lastRow = sheet.getLastRow();
sheet.insertRowBefore(1);
var newLogDateRange = sheet.getRange(1, 1);
var newLogTextRange = sheet.getRange(1, 2);
var now = new Date();
newLogDateRange.setValue(now);
newLogTextRange.setValue(log)
}
When I published the app and followed the generated link I saw my html page with a Click Me button. The click event ran the getSomeData() function which called google.script.run function. The server side .testForWebApp() gotten executed because I've gotten a log entry from myLog() but the .withSuccessHandler or .withFailureHandler were never called. At the same time the myLog() that should be executed after google.script.run never run either.
I definitely don't understand how it works and suspect that if I publish a script as a web app the HTML is not bounded to the script anymore, but I couldn't find any information about it online.
Thanks for your help.
Firstly, you cannot call server-side myLog() function from your client side javascript unless you call it using google.script.run.myLog() Therefore
myLog("in WebAppTest.html getSomeData()");
in your getSomeData() doesnt log anything in your google sheet
Secondly, this code in function onSuccess(testParam)
if (sectionName == null)
is causing your function to terminate prematurely, since there is no variable called sectionName defined.
Note: You can monitor all these errors in the console of your web browser.
Below is the modified code that should work as you intend it to
Final code:
Web App Test
output
function getSomeData()
{
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(showError)
.testForWebApp();
console.log("in WebAppTest.html getSomeData()"); //Log it on the browser console
}
function onSuccess(testParam)
{
var div = document.getElementById('output');
if (testParam == null) // Changed it to testParam from sectionName, to check the value returned from testWebApp()
div.innerHTML = "<p style='color:red;'>You didn't hit the script</p>";
else
div.innerHTML = "<p style='color:black;'>Success:" + testParam + "</p>";
}
function showError()
{
var div = document.getElementById('output');
div.innerHTML = "<p style='color:red;'>You didn't hit the script</p>";
}
Edit
One last note, the below code would make the return text invisible as the text and background color would be the same color (white):
div.innerHTML = "<p style='color:white;'>Success:" + testParam + "</p>";
hence changed the text color to black in the final code
Hope that helps!
Try redeploying the web app, but under a new project version.

Categories

Resources