Handling onSelectionChange event causes further fires onSelectionChange event - google-apps-script

I wanted to select an entire row when the user selects any cell / area of the google sheet. It works fine. Here is the code:
function onSelectionChange(e) {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange();
var rangelist = sheet.getActiveRangeList().getRanges();
var newlist = [];
var cell = sheet.getActiveCell();
for (var i = 0; i < rangelist.length; i++) {
var row = rangelist[i].getRow();
var lines = rangelist[i].getLastRow() - row + 1;
newlist.push(sheet.getRange(row, 1, lines, 11).getA1Notation());
}
sheet.setActiveRangeList(sheet.getRangeList(newlist));
sheet.setCurrentCell(cell);
}
But the current cell gets shifted to the first column of the last selected row. I am afraid that by changing the selection, onSelectionChange is fired again.
In EXCEL, we could supress this by setting Application.EnableEvents as False before changing the selection and setting it back to True after the selection done. How can we solve this problem in Google Sheet?

This is a bug. From the trigger restrictions, "Script executions and API requests do not cause triggers to run."
I think it would be worth filing a bug report in the Issue Tracker.

I played around with this for a while. I agree the last setActiveRangeList generates a new onSelectionChange. I'm not sure why it doesn't keep doing it for an endless loop but I'm glad it doesn't. It would be nice to have a way to inhibit the the onSelectionChange trigger if you wish. Perhaps you should request a new feature. I did it a little different but it's essentially the same thing that you have. I just tried to use the event object parameters where possible to try to minimize the number of functions. I used r.getNumRows() to get the rows in the range.
function onSelectionChange(e) {
//e.source.toast('entry');
//Logger.log(JSON.stringify(e));
const sh=e.range.getSheet();
//sh.getRange(1,1).setValue(JSON.stringify(e));//just a copy of e
const l=sh.getActiveRangeList().getRanges();
let al=[];
l.forEach(function(r,i){
al.push(sh.getRange(r.getRow(),1,r.getNumRows(),11).getA1Notation());
});
//sh.getRange(2,1).setValue(al.join(','));//a copy of the rangelist
sh.setActiveRangeList(sh.getRangeList(al));
}
Diego has a good point I didn't remember that from the restrictions so perhaps submitting it as an issue will fix the problem instead of requesting a feature.

Related

Gapps Script in Google Sheet Is Not Scrolling No Matter What I Do

I am having troubles make Gapps Script scroll the window.
Basically, I want to send the user to the first non blank row from the buttom. My sheet has around 24000 rows. The first non blank row from the bottom is on 23500 row.
I'm using this script but not matter how many time I flush, nothing happens.
I am correctly getting the address of the lastFilledRow... It's just the sheet which never scrolls.
My code is:
function go_to_last_row() {
var ss = SpreadsheetApp.openById('abc.....')
var sheet = ss.getSheetByName('All Leads')
var lastRow = sheet.getMaxRows()
Logger.log(lastRow)
sheet.getRange('A'+lastRow).activate()
SpreadsheetApp.flush()
var lastFilledRow = sheet.getActiveCell().getNextDataCell(SpreadsheetApp.Direction.UP).activate().getA1Notation()
SpreadsheetApp.flush()
Utilities.sleep(3000)
sheet.setActiveRange(sheet.getRange(lastFilledRow)).setValue('hello')
SpreadsheetApp.flush()
Logger.log(lastFilledRow)
Logger.log('done')
}
Any ideas how to force the window to actually scroll? I even set the value "hello" correctly, but still, the cursor never goes to that cell....
Use SpreadsheetApp.getActive(). For some reason, SpreadsheetApp.openById() doesn't provide the same privileged context that container-bound scripts afford. This is actually documented by Google, but in a relatively cryptic way
Functions that are run in the context of a spreadsheet can get a reference to the corresponding Spreadsheet object by calling this function.
As a result of your question, I now interpret that to mean that the other open methods (those that don't include "Active") will not give you that reference to the corresponding Spreadsheet.
This is an abridged version of your code. Works with getActive(), but not openById().
function go_to_last_row() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('All Leads');
var lastRow = sheet.getMaxRows();
sheet.getRange('A'+lastRow).activate();
}

Google Scripts " e.changeType=='INSERT_ROW' " functionality - Reading when a new row is inserted

I'm trying to get my Google sheet to perform certain functions only when a new row is added. After some research I found the .changeType function which seems to work, but I'm having trouble then getting more functions to run based on that condition.
As you can see in my code, I'm using the .changeType function within an if statement.
The if statement appears to work, and it works up until the ui alert - which also works. However, none of the code below it does.
The weird part is that the same code works if I take it outside of the if function.
I'm having a lot of trouble debugging this as the Google Scripts debug tool thinks if(e.changeType=='INSERT_ROW') is an incorrect line of code, but I don't feel it is as it does actually run correctly in the Google sheet. I think this is something to do with the on change trigger functionality.
Perhaps it's the rest of my code that's incorrect, but I can't understand why it works when it's not within that if statement.
What I'm trying to do is to get the row that's just been added, by getting the active range of cells. Then, find the range of cells from immediately below that cell right to the last populated cell in the whole sheet. Those cells should then move with the .moveTo function.
Can anyone explain what's going wrong here? it's been impossible to debug so far!
Code is below:
function moveUpRows(e)
{
var sheet = SpreadsheetApp.getActiveSheet();
var ui = SpreadsheetApp.getUi();
if(e.changeType=='INSERT_ROW')
{
ui.alert("new row added!");
var range = sheet.getActiveRange();
var belowActiveRange = range.offset(1, 0);
var rangeMovingUp = sheet.getRange("L"+belowActiveRange.getRow()+":S"+sheet.getLastRow())
rangeMovingUp.moveTo(rangeMovingUp.offset(-1, 0));
}
}
If you insert a new row - this will lead to an undefined active range notation
In fact, Logger.log(range.getA1Notation()); will log #REF!
You can work around this limitation by defining:
...
var range = sheet.getActiveRange();
var row = range.getLastRow();
var belowActiveRange = sheet.getRange(row+1, 1, 1, sheet.getLastColumn());
...

Why does getActiveRange() not work and are there alternatives?

When I try to invoke the sheet.getActiveRange() function it does not return a range that represents the currently selected cells of the sheet I am using. Instead it usually thinks that the active range is cell(1, 1) but that is not true. The function worked for a few minutes after not working but as since reverted to the same problem. This issue seems documented here1 but there was no answer so I'm bumping the issue again.
I've tried using activate() on getActiveRange() but it only changes my on-screen selection to cell(1, 1) which clearly illustrates the problem.
var rng = master.getActiveRange();
Logger.log(rng.getHeight());
Hypothetically the selected range could have a height of 5 or whatever but it always comes back as one because cell(1, 1) has a height of one. Basically it just always thinks the active range is cell(1, 1).
I was testing this and I kept having the same behavior you are, turns out if you're using SpreadsheetApp.getActiveSheet() after some time you need to "update" it. To do this you simply need to go to the sheet and open the script editor from tools -> script editor and when you run the code then it will be updated.
Additionally, I ran my test using the Selection class, which offers a good example on how to manage it.
For a quick test, you can use the following code:
function selection(){
var selection = SpreadsheetApp.getActiveSheet().getSelection();
var activeRange = selection.getActiveRange();
Logger.log("Active Cell: " + selection.getCurrentCell().getA1Notation())
Logger.log("Range: " + activeRange.activate().getA1Notation());
}
This function will get the active ranges on the active sheet.
function runOne() {
var rl=SpreadsheetApp.getActiveRangeList().getRanges();
var ls='';
for(var i=0;i<rl.length;i++) {
if(i>0) {
ls+=', ';
}
ls+=Utilities.formatString('\'%s\'!%s',rl[i].getSheet().getName(),rl[i].getA1Notation());
}
SpreadsheetApp.getUi().alert(ls);
}

Trigger an email when a cell is written into from another app (IFTTT)

So here's what I've been working on. I'm a basketball coach and have a spreadsheet that pulls in all of my players' tweets from IFTTT.com (it basically takes the RSS feed of a twitter list and when it is updated, it updates the spreadsheet).
I have been working on coding that basically says "if a player tweets an inappropriate word, email me immediately."
I've got the code figured out that if I just type in an inappropriate word, it'll turn the cell red and email me. However, I have not figured out how to get the code to email me after IFTTT automatically updates the spreadsheet with tweets.
Here is my code thus far. Right now I've just got one "trigger" word that is "players" just to try and get the spreadsheet to work. Here's the code:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();//Get the spreadsheet
var sheet = ss.getActiveSheet()//Get the active sheet
var cell = ss.getActiveCell().activate();//Get the active cell.
var badCell = cell.getA1Notation();//Get the cells A1 notation.
var badCellContent = cell.getValue();//Get the value of that cell.
if (badCellContent.match("players")){
cell.setBackgroundColor("red")
MailApp.sendEmail("antadrag#gmail.com", "Notice of possible inappropriate tweet", "This tweet says: " + badCellContent + ".");
}
}
Here is a link to the spreadsheet I'm working with right now: https://docs.google.com/spreadsheets/d/1g5XaIycy69a3T2YcWhcbBy0hYrxSfoEEz8c4-zP63O8/edit#gid=0
Any help or guidance on this is greatly appreciated! Thanks!
I originally wrote this answer for your previous question, so it includes answers to some of your comments from there, but since you're continuing to go asking the community to write this step-by-step , here's the next step.
The issue I'm running into is that if three tweets come into the spreadsheet at the same time, with my code, it's only going to update the most recent cell, not all three. Does that make sense?
Yes, it does make sense.
When an onEdit() trigger function calls Spreadsheet Service functions to get current info from the sheet, it enters a "Race condition". If any changes occur in the sheet after the change that triggered onEdit(), and the time when it gets scheduled, those changes will be visible when it runs. That's what you see when you assume that the change you're processing is in the last row - by the time you're processing it, there may be a new last row.
Good news, though - the attributes of the event object passed to onEdit contain the details of the change. (The parameter e.) See Event objects.
By using e.range and e.value you'll find you have the location and content of the edited cell that kicked the trigger. If additional tweets arrive before the trigger is serviced, your function won't be tricked into processing the last row.
In new sheets, the onEdit() can get triggered for multiple-cell changes, such as cut & paste. However unlikely that it may happen, it's worth covering.
Well, after getting the spreadsheet all setup & actually using the trigger from IFTTT, it doesn't work. :( I'm assuming it's not dubbing it as the active cell whenever it automatically pulls it into the spreadsheet. Any idea on a workaround on that?
Q: When is an edit not an edit? A: When it's made by a script. In that case, it's a change. You can add an installable on Change function to catch those events. Unfortunately, the change event is less verbose than an edit event, so you are forced to read the spreadsheet to figure out what has changed. My habit is to have the change handler simulate an edit by constructing a fake event (just as we'd do for testing), and passing it to the onEdit function.
So give this a try. This script:
handles a list of "bad words". (Could just as easily be monitoring for mentions of your product or cause.)
has an onEdit() function that uses the event object to evaluate the row(s) that triggered the function call.
colors "bad" tweets
has a function for testing the onEdit() trigger, based on How can I test a trigger function in GAS?
includes playCatchUp(e), an installable trigger function (change and/or time-based) that will evaluate any rows that have not been evaluated before. Script property "Last Processed Row" is used to track that row value. (If you plan to remove rows, you'll need to adjust the property.)
Has the sendMail function commented out.
Enjoy!
// Array of bad words. Could be replaced with values from a range in spreadsheet.
var badWords = [
"array",
"of",
"unacceptable",
"words",
"separated",
"by",
"commas"
];
function onEdit(e) {
if (!e) throw new Error( "Event object required. Test using test_onEdit()" );
Logger.log( e.range.getA1Notation() );
// e.value is only available if a single cell was edited
if (e.hasOwnProperty("value")) {
var tweets = [[e.value]];
}
else {
tweets = e.range.getValues();
}
var colors = e.range.getBackgrounds();
for (var i=0; i<tweets.length; i++) {
var tweet = tweets[i][0];
for (var j=0; j< badWords.length; j++) {
var badWord = badWords[j];
if (tweet.match(badWord)) {
Logger.log("Notice of possible inappropriate tweet: " + tweet);
colors[i][0] = "red";
//MailApp.sendEmail(myEmail, "Notice of possible inappropriate tweet", tweet);
break;
}
}
}
e.range.setBackgrounds(colors);
PropertiesService.getDocumentProperties()
.setProperty("Last Processed Row",
(e.range.getRowIndex()+tweets.length-1).toString());
}
// Test function, adapted from https://stackoverflow.com/a/16089067/1677912
function test_onEdit() {
var fakeEvent = {};
fakeEvent.authMode = ScriptApp.AuthMode.LIMITED;
fakeEvent.user = "amin#example.com";
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
// e.value is only available if a single cell was edited
if (fakeEvent.range.getNumRows() === 1 && fakeEvent.range.getNumColumns() === 1) {
fakeEvent.value = fakeEvent.range.getValue();
}
onEdit(fakeEvent);
}
// Installable trigger to handle change or timed events
// Something may or may not have changed, but we won't know exactly what
function playCatchUp(e) {
// Check why we've been called
if (!e)
Logger.log("playCatchUp called without Event");
else {
// If onChange and the change is an edit - no work to do here
if (e.hasOwnProperty("changeType") && e.changeType === "EDIT") return;
// If timed trigger, nothing special to do.
if (e.hasOwnProperty("year")) {
var date = new Date(e.year, e.month, e["day-of-month"], e.hour, e.minute, e.second);
Logger.log("Timed trigger: " + date.toString() );
}
}
// Find out where to start processing tweets
// The first time this runs, the property will be null, yielding NaN
var lastProcRow = parseInt(PropertiesService.getDocumentProperties()
.getProperty("Last Processed Row"));
if (isNaN(lastProcRow)) lastProcRow = 0;
// Build a fake event to pass to onEdit()
var fakeEvent = {};
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
var numRows = fakeEvent.range.getLastRow() - lastProcRow;
if (numRows > 0) {
fakeEvent.range = fakeEvent.range.offset(lastProcRow, 0, numRows);
onEdit(fakeEvent);
}
else {
Logger.log("All caught up.");
}
}

Why doesn't the trigger work for the script running a function on my google spreadsheet?

So I'm using a script on my googledoc spreadsheet that goes through a column and counts a certain number of occurrences of specified formatting. (i.e. in my spreadsheet "=myFunction()")
The function works fine, but my problem is despite setting up a trigger to run the script "onEdit", it never does it. I have to open the script up and save it each time I want it to update on my spreadsheet.
I've been looking up for hours but no one seems to have my question. There are no errors sent by the notifications. The code (though I don't think it's terribly relevant) for my function is:
function CountIfNotStrikeThrough2()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mysheet = ss.getActiveSheet();
var mydatarange = mysheet.getRange(1,1,390,1);
var numRows = mydatarange.getLastRow();
var rowindex = mydatarange.getRowIndex();
var columnindex = mydatarange.getColumnIndex();
var total =0;
for(i=rowindex;i<=numRows;i++)
{
if(mydatarange.offset(i-1, columnindex-1, 1, 1).isBlank() != true && mydatarange.offset(i-1, columnindex-1, 1, 1).getFontLine() != "line-through")
{
total++;
}
}
return total;
}
I know there's a thread here that has a comprehensive list of when the onEdit trigger doesn't activate. (here) For example, if you setup Data Validation and select an item from the list of values in the validation, it doesn't trigger the onEdit function.
Besides that, could you try a simple trigger instead of installable? This is to say, instead of explicitly associating CountIfNotStrikeThrough2() to the onEdit trigger, please try re-naming the function to "onEdit()" instead and see if that works. This creates an implicit or simple trigger. I've had trouble in the past with installable vs. simple triggers.
Also, please share the exact action you're performing to test the trigger.
Reference: https://developers.google.com/apps-script/understanding_triggers