Why does getActiveRange() not work and are there alternatives? - google-apps-script

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);
}

Related

How to tell a script which row to start output

I am using a script to output the date and time that a row was last updated on a Google Sheet. It seems to work just fine, but I want it to only output beginning at the second row, since my first row is a header row full of labels for the columns. I can't seem to figure it out.
The script is from here: https://www.wikihow.com/Google-Sheets-How-to-Insert-Time-in-Cell-Automatically#Script-Editor
This is how it looks in my Apps Script:
/** #OnlyCurrentDoc */
function onEdit(e){
const sh = e.source.getActiveSheet();
sh.getRange ('A' + e.range.rowStart)
.setValue (new Date())
.setNumberFormat ('MM/dd/yyyy HH:MMam/pm');
}
I think I need to define rowStart, but I'm not sure how to do that and search engines haven't pulled up answers I understand.
I tried appending rowStart(2) and rowStart > 0 in place of the original rowStart, which produced errors and made the whole script stop working. I also tried the below with the same response.
rowStart(
value : 2
) : 2;
From here: https://help.grapecity.com/spread/SpreadJSWeb/JavascriptLibrary~GcSpread.Sheets.PrintInfo~rowStart.html
I am new to Google Apps Script (and any scripting), though a longtime Excel and Sheets user. So please explain it to me like I am five. 😅 Branching out!
OK, so this script, as it says on the original wikihow page, will:
insert a timestamp into the specified column any time you enter data into a cell, in the same row as the data you entered. For instance, if you type something into cell A2, a timestamp will appear in cell M2.
So essentially the way that script works, at least per my reading:
It is defining an onEdit handler which handles an edit event every time a user makes any change to any cell in the spreadsheet
the edit event (called e in the script) has a bunch of information about the edit on it, including the row number of the first row that was edited (e.range.rowStart)
the script contains a hard-coded column letter (in your case, A, in the example on wikihow they used M)
every time any edit is made , it goes to the cell identified by the hardcoded column letter and the 1st row that was edited, and it inserts the current date into that cell.
First of all, since this is JavaScript, you can paste your code into a text editor that supports JavaScript, like VSCode for example, and ask it to format it for you. This might bring some clarity to the code because the text editor knows what the syntax means, so it can give you some hints about whats going on or at very least it can make it look nicer:
/** #OnlyCurrentDoc */
function onEdit(e) {
const sh = e.source.getActiveSheet();
sh.getRange('A' + e.range.rowStart)
.setValue(new Date())
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
Second of all, single letter variable names and abbreviations are often frowned upon because they can prevent us from knowing what's going on. And sometimes code can be clarified by giving names to things that might otherwise not be obvious at all. So I will do some naming in this code:
/** #OnlyCurrentDoc */
function onEdit(editEvent) {
const sheet = editEvent.source.getActiveSheet();
const rowThatWasEdited = editEvent.range.rowStart;
const columnThatHoldsTheLastEditedDates = 'A';
const correspondingCell = columnThatHoldsTheLastEditedDates + rowThatWasEdited;
const rightNowDate = new Date();
sheet.getRange(correspondingCell)
.setValue(rightNowDate)
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
Finally, since this is javascript, we have access to all of the javascript features like if statements, loops, functions, etc. Since you want to make it ignore the header row, I would recommend an if statement: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else
if we are on the header row, do nothing. Otherwise, do the normal thing.
/** #OnlyCurrentDoc */
function onEdit(editEvent) {
const sheet = editEvent.source.getActiveSheet();
const rowThatWasEdited = editEvent.range.rowStart;
if(rowThatWasEdited > 1) {
const columnThatHoldsTheLastEditedDates = 'A';
const correspondingCell = columnThatHoldsTheLastEditedDates + rowThatWasEdited;
const rightNowDate = new Date();
sheet.getRange(correspondingCell)
.setValue(rightNowDate)
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
}

Handling onSelectionChange event causes further fires onSelectionChange event

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.

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());
...

GOOGLE script 'copyTo values only' does not work when the source is a function (e.g. NOW())

I am trying to copy the values only from a cell containing the function NOW().
For everything I try the target cell is empty :-(
I have tried:
spreadsheet.getRange('K1').activate();
spreadsheet.getCurrentCell().setValue('Erstellt am:');
spreadsheet.getRange('P1').activate();
spreadsheet.getCurrentCell().setFormula('=NOW()');
spreadsheet.getRange('N1').activate();
spreadsheet.getRange('P1').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
spreadsheet.getActiveRangeList().setNumberFormat('dd.MM.yyyy HH:mm:ss');
The result is that cell N1 is empty. If I add the statement:
spreadsheet.getCurrentCell().setValue('OTTO');
Then the cell N1 gets the value OTTO as expected.
I also tried outsourcing the logic into a separate function like this:
function COPY_DATE() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('N1').activate();
spreadsheet.getRange('P1').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
spreadsheet.getActiveRangeList().setNumberFormat('dd.MM.yyyy HH:mm:ss');
};
This did not work either.
I also tried this way:
var source = spreadsheet.getRange ('P1');
source.copyTo (spreadsheet.getRange ('N1'), {contentsOnly:true});
Everything to no avail. I would really like to know what's going on here and appreciate any feedback.
To be honest, I have no idea why SpreadsheetApp.CopyPasteType.PASTE_VALUES isn't working here.
Instead of doing that, try a different approach: use getValue() and setValue().
spreadsheet.getRange('N1').setValue(spreadsheet.getRange('P1').getValue())
I'm not sure if it makes sense for your purposes, but I'd also suggest you look at removing a lot of those .activate() calls. As I wrote in this answer, activation is basically just for interacting with a user selection, and it can slow down the execution. The way you're doing it, you're calling the Spreadsheet service 4 times:
get the range
activate the range
get the activated range
do something to the activated ranged
You can halve those calls by simply getting the range and then perform whatever actions you need on it. Then you could simplify your code to something like this:
spreadsheet.getRange('K1').setValue('Erstellt am:');
var value = spreadsheet.getRange('P1').setFormula('=NOW()').getValue();
spreadsheet.getRange('N1').setValue(value).setNumberFormat('dd.MM.yyyy HH:mm:ss');
I tested the following code and is working perfect setting the date value from P1 cell to N1 cell:
function COPY_DATE() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('N2').activate();
spreadsheet.getRange('P1').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
spreadsheet.getActiveRangeList().setNumberFormat('dd.MM.yyyy HH:mm:ss');
};
Remember that to use getActive() type of functions you need to have the script bound to the Spreadsheet [1].
[1] https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#getactive

Highlight entire row when cell is active

How to design a sheet script that would result in an active row being highlighted?
I would like to have an entire row change color of font or background when one cell in that row is active.
I don't want the trigger to be any specific value in the cell, just clicking on a cell should trigger the highlight for the whole row that cell belongs to.
Sorry, this can't be done with conditional formatting or script by just selecting a cell. You can, however, highlight an entire row of the active cell with the key combination Shift+Spacebar.
I realize this question was asked a while ago, but I stumbled upon it when I was also looking for this same function. My solution is a little cumbersome and isn't a full solution to what you're looking for, but it combines both a tiny script and a little conditional formatting.
I first wrote a small script using the onEdit() function:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var targetCell = sheet.getRange('AD1');
var activeCell = e.range.getA1Notation();
targetCell.setValue(activeCell);
}
I chose 'AD1' as the target cell, as it was far out of the way and, if need be, I could also choose to hide that column.
Then I went over to the conditional highlighting and typed this in as a custom formula:
=ROW()=ROW(INDIRECT($AD$1))
Voila! Every time I edit a cell, it automatically highlights that entire row.
It's not exactly what you're looking for, as it won't automatically highlight the entire row as soon as you click on a cell ... only when you edit the cell. Also, if you have other formulas running and other conditional formatting going on, your spreadsheet can start to get slow. But this is the closest I've seen out there to a possible solution.
Much less as cool, but still somewhat functional regarding legibility is a basic highlighting of every other row. For example:
in conditional formatting: =ROW()=EVEN(ROW())
You can use the onSelectionChange event, like this.
In my case, row 1 had some title cells in it with their own background colors. I only highlight the current row if you're in row 2 or later.
function onSelectionChange(e) {
const range = e.range;
const sheet = range.getSheet();
const maxRows = sheet.getMaxRows();
const maxColumns = sheet.getMaxColumns();
// Clear the background color from all cells.
// (Except row 1 - that has my titles in it)
sheet.getRange(2, 1, maxRows - 1, maxColumns).setBackground(null);
// Don't set the background color if you're on the first row
if (range.getRow() > 1) {
// Highlight the current row
sheet.getRange(range.getRow(), 1, 1, maxColumns).setBackground("#c9daf8");
}
}
It takes a second or so to update - I guess that event's a little slow firing.
This works on desktop or touch device.
The problem you describe can be solved indirectly through the checkbox.
Insert column A in the table.
In column A, select cells in the rows you want to highlight with color.
From the Insert menu, choose Checkbox.
Select entire rows in which the check box has been inserted.
From the Format menu, choose Conditional Formatting.
In the Formatting rules panel, add the Custom formula to this rule.
Enter the formula =$A1=TRUE (instead of 1, use the first line number you selected in step 4).
Specify the Formatting style.
From now on, after selecting the check box, the entire row will be highlighted.
Sadly this cannot be done via onFocus as we all would prefer, but this works well enough for me using the onEdit event. It's still oddly slow, so perhaps someone could make it faster (certainly from reading / writing to properties, but that's the only way I found to track which row is highlighted).
function onEdit(e){
manageRowHighlight(e);
}
function manageRowHighlight(e) {
var props = PropertiesService.getScriptProperties();
var prevRow = parseInt(props.getProperty('highlightedRow'));
var range = e.range;
var thisRow = range.getRow();
//if it's same row, just ignore
if (prevRow == thisRow) {
return;
} else if (prevRow != null){
//else unhighlight it
range = range.getSheet().getRange(prevRow + ':' + prevRow);
range.setBackground(null);
}
//highlight the current row
var range = range.getSheet().getRange(thisRow + ':' + thisRow);
range.setBackground('#fff2cc')
//save the row so highlight can be removed later
props.setProperty('highlightedRow', thisRow);
};
Reading spreadsheets is difficult when there are many columns. When selecting one cell/row, it will highlight the entire row, otherwise, it won't bother:
function onSelectionChange(e) {
const sht = SpreadsheetApp.getActive().getActiveSheet();
const rowCount = sht.getActiveRange().getNumRows();
const maxRows = sht.getMaxRows();
const maxColumns = sht.getMaxColumns();
sht.getRange(2, 1, maxRows - 1, maxColumns).setBackground(null); //skip the first row (headers)
if (rowCount == 1) {
const myrow = sht.getActiveRange().getRow();
if (myrow > 1) { //don't paint the header row
sht.getRange(myrow, 1, 1, maxColumns).setBackgroundRGB(230,230,130);
}
}
}
A workaround that works wonderfully for me was install this app https://www.autocontrol.app/ to redefine the normal behavior of keys into shift+space combination which is actually the shortcut for selecting the current row.
In summary: I changed the behavior of down/up arrows to synthesize a new keyboard input as down-arrow + shift + space. This is the procedure:
After install the extension create a new rule and use the down arrow as trigger
Set the behavior to only occurs in Google Sheets by checking if the URL starts-with https://docs.google.com/spreadsheets/
In the action section select synthesize input (go to advance options>others)
The specific synthesize is: down arrow, then make the combination shift+space. Do it exactly in this way. The app will show you four buttons.
In the synthesize box click the space key and set it to action 2 times (this is to select the full row and not only partially in some cases, yet I suggest you to try using "1 times")
Repeat the process with the UP arrow.
That's all. It took me a time at first but after knowing the trick it is easy. I added an image of how this setting looks like.
Note: Why does the GUI show four buttons in the synthesize box: arrow, shift, space and shift again? This is the expected behavior due to press/release events.
Note2: If the extension in general seems not to be working check the "emergency repair" with right click over the extension icon, or just write the developers.
I'm not a javascript expert, but this can be done fairly easily by modifying directly the document on reaction to specific events. I looked a bit at the current Sheets HTML and running the following code in the JavaScript console makes it highlight on mouse click:
function f() { let mylist = document.getElementsByClassName("selection"); for (let i=0; i<mylist.length; i++) { if (parseInt(mylist[i].style.height)>0 && parseInt(mylist[i].style.width)>0) { mylist[i].style.left='0px'; mylist[i].style.width='100000px'; mylist[i].style.display=null; break; }; }; }; onclick = f;
You can open the console with F12 on Chrome, then Click on the Console tab. You can close it afterward (F12 or X button), the change will remain in effect until the page is closed or reloaded.
Of course Google can change the HTML & CSS at any time and break this, so ideally if this is really needed someone should maintain an extension and keep it updated with Sheet changes (there may be already generic extensions where this could be added easily, please comment if you know any).
The expanded and commented code, if you're interested in modifying or fixing it later:
function f() {
// Get all elements with class "selection"; there should be just a few
// of them, and almost all should be have 0-width or 0-height
let mylist = document.getElementsByClassName("selection")
for (let i=0; i<mylist.length; i++) {
// Find the first element with an area (usually just one)
if (parseInt(mylist[i].style.height)>0 && parseInt(mylist[i].style.width)>0) {
// Set left to 0px (it's >0 when you click on other columns than A)
mylist[i].style.left = '0px';
// Set width to something large so it take the whole sheet
mylist[i].style.width = '100000px'; // NB: very wide sheets could need more!
// Undef the display attribute (set to 'none' by default to hide selection color)
mylist[i].style.display = null;
// On multi-row selections we may match more than one element,
// changing anything but the first breaks the sheet
break;
};
};
};
// Run the function on every mouse click
onclick = f;