Highlight entire row when cell is active - google-apps-script

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;

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

Dynamic Page Breaks in Google Sheets

I have mild general programming knowledge but know basically nothing about google apps scripts specifically.
I am trying to create dynamic page breaks in google sheets, or find another way to keep certain rows grouped together when printing.
On my data sheet I have 100s of rows of information and within each row the data can vary significantly in length (from a single number to many paragraphs of text). I have created a second sheet that both filters the information that I want and displays it in a visually-appealing way, taking the original data from each row and parsing it into 8 total rows (7 with information, and one blank to visually separate one block of info from the next) per one original. The problem is that the varying length of the data means I have to manually move the page breaks every time I change the filter.
Here is a blank section of the second sheet for reference.
I want to be able to print with as many 8-row groupings on a page as I can, but not split up a group onto the next page.
I'm honestly not sure how to get started, though I presume that I can use the blank row to trigger the page breaks somehow. Any help would be greatly appreciated!
Updated
I have been able to write some rudimentary code to (mostly) accomplish what I wanted. However the best that I can tell is that getRowHeight() is not working with my wrapped text, as it properly formats when I have any empty data set, but not otherwise.
Can someone confirm, and tell me what I'm missing?
function dynamicPageBreaks() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var curRow = 2; //start on Row 2 (1 contains the filter selection)
var curTotPix = ss.getRowHeight(curRow);
var pgPix = 1030;
//loop until end of sheet
var endRow = ss.getLastRow();
do {
//find row that goes past page break
do {
curRow++;
curTotPix = curTotPix + ss.getRowHeight(curRow);
} while (curTotPix <= pgPix);
//get value of cell in column B of that row
var curCell = sheet.getRange(curRow,2).getValue();
//back up until we find an empty row
if (curCell == "") {
break;
} else do {
curTotPix = curTotPix - ss.getRowHeight(curRow);
curRow = curRow - 1;
curCell = sheet.getRange(curRow,2).getValue();
} while (curCell != "");
//expand empty row to match necessary pixels
var addHeight = pgPix - curTotPix;
ss.setRowHeight(curRow - 1, ss.getRowHeight(curRow) + addHeight);
//reset for next iteration
curTotPix = ss.getRowHeight(curRow);
} while (curRow < endRow);
}
I would recommend you get started by following an Apps script quickstart like the one about extending sheets here[1]
Then you can define your logic using methods from the Spreadsheet service[2].
You have the method getRowHeight()[3] which could help you in determining the length of the range you are looking to print. And that also depends on the paper size you choose.
[1]https://developers.google.com/apps-script/guides/sheets#get_started
[2]https://developers.google.com/apps-script/reference/spreadsheet
[3]https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrowheightrowposition

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

Get the row/col of a clicked drawing?

I assign to a cell in each row a png icon and a script with
sheet.insertImage(blob, 11, i).assignScript("ClickMe");
At the end of the process I have a few hundred rows with buttons at the end. What I'm struggling to do at the moment is work out which row gets clicked on. I've tried a few likely candidates like
var ss = SpreadsheetApp.getActiveSheet();
var sheetName = ss.getSheetName();
var activeRange = ss.getActiveRange();
var selection = ss.getSelection();
var ar = activeRange.getA1Notation();
var cc = ss.getCurrentCell().getA1Notation();
var scr = selection.getCurrentCell().getA1Notation();
var ac = ss.getActiveCell().getA1Notation();
Logger.log("scr=%s, ar=%s, cc=%s, ac=%s", scr, ar, cc, ac);
but having clicked on the 9th line, the Logger.log says
[19-06-20 22:39:01:123 HKT] scr=A1, ar=A1, cc=A1, ac=A1
So the question is this: if someone clicks on the button on the 21st line, can the script figure out what line the click occurred on?
The other answer is that you need to make 21 helper functions,
what I imagine you have now (> binding script)
Button1 > script
Button2 > script
Button3 > script
Button4 > script
function script(){
//Do something
}
Instead you need
Button1 > scriptOne
Button2 > scriptTwo
Button3 > scriptThree
Button4 > scriptFour
with carrier functions:
function scriptOne (){script(1);}
function scriptTwo (){script(2);}
function scriptThree (){script(3);}
function scriptFour(){script(4);}
and then you have that info in the script function
function script(input){
var row = input;
//Do something
}
This does get tedious, and isn't exactly easy to generate in bulk, but if you have a fixed sheet, that might work.
You can also consider making the button a checkbox, and then making your script be "onEdit" and checking for which row the change occurred in. But the way you are currently thinking won't work because there is no way to bind the scripts passing a variable from the button itself. The script will see all of your current buttons as the exact same thing.
It’s not possible to achieve exactly what you want in your code. This is because when you use the functions “getActiveRange” and “getSelection” [1], it returns the cell or cells that are being selected (have the focus on it). When you click in the inserted image, you’re not clicking the cell but only the image, so the focus is still in the first cell.
To obtain the cell where the image is anchored you can use the following snippet:
var image = sheet.insertImage(blob, 11, i).assignScript("ClickMe");
image.getAnchorCell().getA1Notation());
The problem is that you can’t pass any parameters in the assignScript function (like the cell position in this case), only the name of the function to run [2].
A possible workaround is to adjust the size of the cell to contain the complete image. Once this is done, request to your users that they must first click in the cell and then in the image:
var image = sheet.insertImage(blob, 11, i).setHeight(20).setWidth(20).assignScript("ClickMe");
[1] https://developers.google.com/apps-script/reference/spreadsheet/range
[2] https://developers.google.com/apps-script/reference/spreadsheet/over-grid-image

Google Spreadsheet: Script to change background color of a cell based on a hex code in the neighboring cell

I'm trying to write a script which will take a column of hex codes and color the adjacent column's background color to be that hex code color, but I'm just not sure how to do it.
I've been looking at these:
1) https://developers.google.com/apps-script/reference/spreadsheet/range#setbackgroundcolor
2) https://developers.google.com/apps-script/reference/spreadsheet/range#getcellrow-column
3) Google Spreadsheet: Script to Change Row Color when a cell changes text;
But haven't really hade any progress. Any help would be appreciated!
Thanks!
Something like this should solve your problem:
function onEdit() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange();
var actCell = sheet.getActiveCell();
var actData = actCell.getValue();
var actRow = actCell.getRow();
if (actData != '' && actRow != 1) //Leaving out empty and header rows
{
range.getCell(actRow, 2).setBackground(actData);
}
}
Although, this only colors one cell at a time, but it portrays how you can set a background color based on cell input. Depending on your use case, whether you are copy-pasting multiple hex codes at a time or whether you have an existing list of hexcodes and you would like to show their color against those, using getCell(row,column).setBackground(String) should be able to help you out.
Here's a solution that will act only on specified columns and will also make sure the text entered is a valid hex color code. You could have it watch one column for changes and apply the background color changes to any other column, not just the immediately adjacent one. You also don't have to worry about cells containing other text, like headers, since this code will ignore them. And you can paste multiple lines into the spreadsheet, as this function will operate on any edited cells in the watched column.
It's worth noting that this code isn't foolproof, and I haven't optimized it. For example, you will probably get an error if you try to change the background color of a cell that is beyond the active sheet's current maximum range (you could easily write code to add new columns to the sheet if necessary). Also, if you paste an extremely large number of rows into the sheet, it's possible the function could time out. Though I doubt this would happen in normal use, the best practice would technically be to apply the changes all at once at the end of the forEach loop instead of one at a time.
You could rewrite this as a menu function, too, if that's what you meant by "take a column of hex codes." The code below only works on newly entered text. For pre-existing text, rename the function and modify it to check the desired column automatically instead of functioning as a trigger (or check the active range if you want to select only certain cells). No muss, no fuss.
The regular expression assumes that a valid hex color code is a string that starts with an octothorpe and is followed by either 3 or 6 characters, each of which must be 0-9, A-F, or a-f.
SpreadsheetApp.flush() may not be strictly necessary, but it's supposed to afford real time updating of changes in this sort of situation.
// regex for hex color codes
HEX_COLOR_REGEX = /(^#[0-9A-Fa-f]{3}$)|(#[0-9A-Fa-f]{6}$)/;
// column to watch for changes (i.e. column where hex color codes are to be entered)
HEX_CODE_COLUMN = 1; // i.e. column A
// column to change when above column is edited
HEX_COLOR_COLUMN = 2; // i.e. column B
// utility function to test whether a given string qualifies as a hex color code
function hexTest(testCase) {
return HEX_COLOR_REGEX.test(testCase);
}
function onEdit(e) {
var range = e.range;
var row = range.getRow();
var column = range.getColumn();
if (column === HEX_CODE_COLUMN) {
var values = range.getValues();
values.forEach( function checkCode(rowValue, index) {
var code = rowValue[0];
if (hexTest(code)) {
var cell = SpreadsheetApp.getActiveSheet().getRange(row + index, HEX_COLOR_COLUMN);
cell.setBackground(code);
SpreadsheetApp.flush();
}
});
}
}