Apply a "row banding" theme to a range - google-apps-script

I am a beginner to Google Apps Script but use it to automate some simple repeating tasks. I have several spreadsheets I am copying content on a weekly basis and export them as an .xls file that I send to my client.
I am trying to apply alternating colors to a range I copy from another sheet but I completely got stuck. How to correctly set bandingTheme with the applyRowBanding method? What is the right syntax I should use in the last line of my code?
My code:
function copyRange (SourceSSID, SourceRange, TargetSheetName, bandingTheme) {
var sheetSource = SpreadsheetApp.openById(SourceSSID);
var sheetTarget = SpreadsheetApp.openById("bla-bla");
var source = sheetSource.getRange(SourceRange);
var target_ss = sheetTarget.getSheetByName(TargetSheetName);
var values = source.getValues();
var target = target_ss.getRange(1, 1, values.length, values[0].length);
target.clear();
target.setValues(values);
target.applyRowBanding ();
}

If your method argument bandingTheme is one of the enums listed here, you can simply apply it, using the apply___Banding(BandingTheme theme) method signature:
target.applyRowBanding(bandingTheme);
The above is equivalent to this line, per documentation:
target.applyRowBanding(bandingTheme, true, false);
(In other words, the default behavior is to color the header but not the footer, in addition to alternating row colors.)
You can ensure no existing themes were previously present (only a single kind of alternating colors - be it from columns OR rows - can be present at any given time, else an error is thrown).
target.getBandings().forEach(function (banding) {
banding.remove();
});
/**
* set the new banding theme
* ....
*/
If you wanted to set a custom banding theme, you can do so by starting from one of the theme designs. Note that the apply___Banding methods return the Banding object that they applied. If you bind this return value (or chain the methods), then you can modify it using its class methods.
const newBanding = target.applyRowBanding(SpreadsheetApp.BandingTheme.BLUE);
// newBanding is now a Banding that was instantiated with the "Blue" template.
// Color the header column:
newBanding.setHeaderColumnColor('teal');
// Equivalent:
target.applyRowBanding(SpreadsheetApp.BandingTheme.BLUE).setHeaderColumnColor('teal');
Note that setting colors for non-header columns in a row-banding theme doesn't work. Likewise for setting non-header row colors in a column-banding theme.
If your bandingTheme argument isn't one of the theme enums, then you will have to provide more details about what it is in order to get answers that help you convert it into the available Spreadsheet Service methods.

Here is a simple function that removes existing bandings and then applies alternating colors to an entire sheet. Please refer to Google Apps Script Range and Banding Class documentation for support.
function applyRowBanding() {
let sheet = SpreadsheetApp.getActiveSheet();
let range = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn());
range.getBandings().forEach(banding => banding.remove());
range.applyRowBanding(SpreadsheetApp.BandingTheme.LIGHT_GREY, true, false);
}

Related

Is it possible to add formatting (shading) to rows being appended in Google Sheets (by Google Apps Script)

I've got a Google App Script which is copying rows from one sheet to another, performing various transformations. This logic ultimately gets rows onto the new sheet using sheet.appendRow(row detail). I would like these newly created rows to have a background colour (my intention is to hold a 'latestColour' so I can alternate the shading).
So, is there anyway to add shading within the appendRow method itself, or easily determine the range that the appendRow method processed, such that I can apply additional logic to add the shading.
You can use conditional formatting
=and(A1<>"",A2="")
Although I'm not sure whether I could correctly understand your situation, from your question, I thought that you might be using [Format] --> [Alternating colors] in Google Spreadsheet. And, when a new row is appended by putting the values, you might want to reflect "Alternating colors" in the appended row. If my guess is correct, how about the following sample script?
Sample script:
function myFunction() {
const addValues = ["sample1", "sample2", "sample3"]; // This is a sample appending value. Please replace this for your value.
const sheetName = "Sheet1"; // Please set the sheet name.
// Retrieve banding object from the data range.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const b = sheet.getDataRange().getBandings();
if (b.length == 0) {
console.log("Bandings are not used.");
return;
}
// Append the value.
sheet.appendRow(addValues);
// Expand the range of banding.
b[0].setRange(sheet.getDataRange());
}
When this script is run, the current banding is retrieved. And, after the value was appended, the banding is updated by including the appended row. In this sample, even when the multiple rows are appended, this script can be used.
Note:
From your question, I guessed that there is one banding in the data range in your sheet. Please be careful this.
References:
getBandings()
setRange(range)
Unfortunately the method appendRow() does not receive formatting settings as input, only an array of values.
However, here is a suggestion if you want to implement your own logic:
Sample code:
function applyColorLastRow() {
var ss = SpreadsheetApp.getActive(); //get active sheets file
var range = ss.getDataRange(); //get populated range, you may want to set a range manually if needed.
var lastRowNum = range.getLastRow(); //getting the last row index of the range.
var lastRowRange = ss.getRange(`${lastRowNum}:${lastRowNum}`); //narrowing the range (using A1 notation) to the last row only to apply color
var lastRowColor = lastRowRange.getCell(1,1).getBackgroundObject().asRgbColor().asHexString();
//Your row coloring logic here...
if (lastRowColor === '#ffffff'){ //toggling white/grey color as an example...
lastRowRange.setBackground('#cccccc'); //apply grey color to all cells in the last row range
} else {
lastRowRange.setBackground('#ffffff'); //apply white color to all cells in the last row range
};
}

Converting formatted Text from Google sheets to Google docs

I am trying to copy formatted texts from google sheets to google docs using google scripts. I have successfully converted text from sheets to docs however I am unable to carry over the relevant formatting/markdowns like bold, italics, colour, underlined & etc. Does anyone have any idea as to what I am doing wrong or what functions I can use in the google scripting library which allows me to copy over the formatting as well?
Currently, I have an existing google doc that acts as the template. All future google docs created will follow a similar template. I have created a sheet named 'doc Builder' and have used ,for loops and switch statements to choose which cell within the sheet to be copied over to the word doc.
function createDocument() {
var docbuilder = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('doc Builder');
//active data range
var range = docbuilder.getRange(4, 1, docbuilder.getLastRow() - 3, docbuilder.getLastColumn() - 1).getDisplayValues();
var templateId = 'myworddocIDwhichihaveremoved'; //the word doc
//Make a copy of the template file
var documentId = DriveApp.getFileById(templateId).makeCopy().getId();
//Rename the copied file
DriveApp.getFileById(documentId).setName('name of new doc');
//Get the document body as a variable
var body = DocumentApp.openById(documentId).getBody();
//copies texts from cell to word doc
//i = row, j = column
for(var i = 0; i < range.length; i++){
for(var j = 0; j < range[i].length; j++){
var cells = [];
switch(j) {
case 0:
body.appendParagraph(range[i][j]);
break;
case 1:
body.appendParagraph(range[i][j]);
break;
case 2:
if(range[i][j] != ""){
body.appendParagraph('PARAGRAPH 1:' + range[i][j]);
}
break;
case 3:
body.appendParagraph('PARAGRAPH 2:' + range[i][j]);
break;
}
}
}
}
I have tried copyTo() and it copies the formatting from sheet to sheet successfully however am unable to do the same for sheet to doc. I am also aware of the attributes which I can add to my word doc like BACKGROUND_COLOR, BOLD and etc from the documentation however the data I am handling often only has some parts of the cell formatted for example : sally is a girl instead of sally is a girl. Thus making it difficult to hard code when the number of cells increases.
Simply put I am trying to bring over the formatting from the sheet to the doc so I don't have to handle each cell individually.
I am working with more cases but I have removed them to simplify the code, also every cell within the active data range is formatted but when the new google doc is created the formatting disappears.
I hope someone has a solution to this haha :""D
Copying values from Sheets to Docs with formatting
There is no native method that you can use to copy formatted text from Sheets to Docs. They don't use the same classes to handle formatted text.
Sheets has RichTextValue that contains all the information for a cell. For example, when you call:
const range = sheet.getRange("A2")
const richTextValue = range.getRichTextValue()
You then can obtain all the information about the text formatting within the cell. For example, if you have a cell like this:
If you get the rich text value from this cell and then call the getRuns() method on this value you will get a series of new RichTextValue:
wherein each run is the longest possible substring having a consistent text style.
So for the example you will get a new object for:
"Hello"
"bold"
"italic"
... etc
You may also get individual object for the spaces between words.
For each of these objects, you can call a series of methods to get the individual components of its format:
getFontFamily()
getFontSize()
getForegroundColor()
isBold()
isItalic()
isStrikethrough()
isUnderline()
NOTE: getBackgroundColor() is not used in this example because background color in sheets cannot apply to single text runs, but the whole cell.
There is no equivalent class in DocumentApp. You can't append a RichTextValue to any element in a document. So this means that you need to match up the corresponding methods that you need. For example, you could use the Text class which has all the corresponding methods, you would just need a go-between to link up the methods and sequence them in the right way.
Example implementation
This would most likely need to be adapted to your exact needs, I don't know what the logic of the switch statements are and I don't have sample data to test it with, but this should give you a good idea of how it might work. You may also be able to use the custom class as-is in your script.
Ideally you would be able to call some simple methods from the main script, something like this:
function main() {
// Getting the rich text value
const sheet = SpreadsheetApp.getActive();.getSheetByName("Sheet1");
const range = sheet.getRange("A2");
const value = range.getRichTextValue();
// Creating an instance of a custom class that will be implemented
const textToExport = new SheetRichText(value)
// Setting the target document
const doc = DocumentApp.openById("[YOUR DOCUMENT ID]")
const body = doc.getBody()
// Calling a method of the custom class
textToExport.appendAsNewParagraph(body)
}
NOTE: Replace [YOUR DOCUMENT ID] with the correct document ID.
Remember that in my example my sheet has this:
The custom class I have implemented in my example is:
class SheetRichText{
// To initialize it you pass it a RichTextValue object
constructor(RichTextValue){
// It gets all the runs and creates an object that contains all the information
// needed to call the corresponding methods in the document Text class.
this.runs = RichTextValue.getRuns().map(run => {
const style = run.getTextStyle()
return {
"style" : {
"fontFamily" : style.getFontFamily(),
"fontSize" : style.getFontSize(),
"foregroundColor" : style.getForegroundColor(),
"bold" : style.isBold(),
"italic" : style.isItalic(),
"strikethrough" : style.isStrikethrough(),
"underline" : style.isUnderline()
},
"text" : run.getText(),
"start" : run.getStartIndex(),
"end" : run.getEndIndex()
}
})
}
// This takes as an argument the body of a document and adds the RichTextValue
// to the document as a new paragraph
appendAsNewParagraph(body){
// Initializing the new blank paragraph
const paragraph = body.appendParagraph("")
// For each run, copy the text and then set all the formatting
// making sure that the start and end indices are called.
this.runs.forEach(run => {
const textElement = paragraph.asText().appendText(run.text)
const [start, end] = [run.start, run.end -1]
textElement.setFontFamily(start, end, run.style.fontFamily)
textElement.setFontSize(start, end, run.style.fontSize)
textElement.setForegroundColor(start, end, run.style.foregroundColor)
textElement.setBold(start, end, run.style.bold)
textElement.setItalic(start, end, run.style.italic)
textElement.setStrikethrough(start, end, run.style.strikethrough)
textElement.setUnderline(start, end, run.style.underline)
})
}
}
Which results in:
References
Sheets RichTextValue
Docs Text

Bug in getBackgrounds() using custom theme colors

I've got a spreadsheet with colors from a custom theme.
When I read the colors using getBackgrounds() the colors returned are all #000000
If I read the colors cell-by-cell using getBackground() the colors are returned correctly.
If I use standard colors (ie not colors in my Theme) the colors are also returned correctly.
TEST SHEET
(available to view at https://docs.google.com/spreadsheets/d/1nCZeUbCjs_5p6_52v8ggqVgrgnJ-Pd6x-gzXUFfV8G0/edit?usp=sharing
Cells A1:D1 contain the names of the four Beatles, all with background color #b70906
TEST CODE
/** #OnlyCurrentDoc */
function getbackgroundstwoways(){
var fullrange= SpreadsheetApp.getActiveSpreadsheet().getRange("A1:D1");
// Read all cells using getBackgrounds
var arBack = fullrange.getBackgrounds();
var arValues=fullrange.getValues()
Logger.log("Full array " +arBack + arValues);
//Now do the cells individually with getBackground
for (var i=0; i<fullrange.getLastRow();i++){
for (var j=0; j<fullrange.getLastColumn();j++){
Logger.log("Single cell " + i + " " + j + " " + fullrange.offset(i,j).getBackground() + " " + fullrange.offset(i,j).getValue() ) ;
}}}
LOGGER OUTPUT
Full array #000000,#000000,#000000,#000000John,Paul,George,Ringo
Single cell 0 0 #b70906 John
Single cell 0 1 #b70906 Paul
Single cell 0 2 #b70906 George
Single cell 0 3 #b70906 Ringo
How about this answer?
Issue and solution:
When I saw your shared Spreadsheet, the background colors of cells "A1:D1" has the color type of "THEME". I think that this is the reason of your issue.
In the current stage, it seems that getBackground() can directly retrieve the background color from the color type of "THEME" as the hex string. But, it seems that getBackgrounds() cannot directly retrieve them. The retrieved values using it becomes #000000. I'm not sure whether this is the bug or the current specification. But in the current stage, the background colors of the color type of "THEME" can be retrieved by the methods in Spreadsheet service.
One of several solution is to use getBackground() as your script. This has already been achieved in your script.
In this answer, as another pattern, the colors are retrieved from the values retrieved by getThemeColors. The flow of this script is as follows.
Create an object for searching the colors from the theme color type.
Retrieve background objects.
Retrieve the background colors from backgroundObjects.
Sample script:
function getbackgroundstwoways() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const fullrange = ss.getActiveSheet().getRange("A1:D1"); // Range from your script.
// 1. Create an object for searching the colors from the theme color type.
const theme = ss.getSpreadsheetTheme();
const themeColorObj = theme.getThemeColors().reduce((o, e) => Object.assign(o, {[e]: theme.getConcreteColor(SpreadsheetApp.ThemeColorType[e]).asRgbColor().asHexString()}), {});
// const fullrange = SpreadsheetApp.getActiveSpreadsheet().getDataRange();
// 2. Retrieve background objects.
const backgroundObjects = fullrange.getBackgroundObjects();
// 3. Retrieve the background colors from backgroundObjects.
const backgroundColors = backgroundObjects.map(r => {
return r.map(c => {
if (c.getColorType() == SpreadsheetApp.ColorType.RGB) {
return c.asRgbColor().asHexString();
} else if (c.getColorType() == SpreadsheetApp.ColorType.THEME) {
return themeColorObj[c.asThemeColor().getThemeColorType()];
} else {
return null;
}
});
});
console.log(backgroundColors);
}
This script can be used for the color types of both "RGB" and "THEME". When this script is run at your shared Spreadsheet, [ [ '#b70906', '#b70906', '#b70906', '#b70906' ] ] can be seen at the log.
Even when the RGB types and the THEME types are mixed, this script can retrieve the background colors as the hex string.
Note:
In this case, the background colors are retrieved from the theme colors. But when you overcoat the background colors as the RGB type of #b70906, you can retrieve them using getBackgrounds().
When I searched about this that getBackgrounds() cannot be used for the THEME color type at the issue tracker, I couldn't find this. So how about reporting this? Ref
Please use this script with V8.
References:
getSpreadsheetTheme()
This method is added at December 18, 2019.
Class SpreadsheetTheme
getBackgroundObjects()

Doesn't sheet.isRowHiddenByFilter(n) work with slicers?

I am trying to use a script to do something to only the rows that are not filtered when using slicers in Google Sheets. I can't get it to work.
I have tried this:
Logger.log(SpreadsheetApp.getActiveSheet().isRowHiddenByFilter(1));
I get "false" even if the row is hidden by using a slicer (new feature in Google Sheets).
Anyone got it to work?
Edit: It works with regular filters, grammar, edited row number in function parameter. Replaced the phrase filter control (direct translation from Norwegian) to slicers, which is the correct english name for the feature.
You want to use isRowHiddenByFilter() for Slicer.
If my understanding is correct, how about this answer? Please think of this as just one of several answers.
By the update of Google side at November 6, 2019, Class Slicer was added. By this, Slicer of Spreadsheet got to be able to be managed with Google Apps Script.
In this answer, I would like to propose the method for using isRowHiddenByFilter() using Class Slicer.
Flow:
Unfortunately, even when Class Slicer was added, isRowHiddenByFilter() cannot be directly used for the Slicer. So as a workaround, the following flow is used.
Retrieve the current Slicer.
Retrieve the range, column position and the filter criteria from the Slicer.
Create new basic filter using the retrieved range, column position and the filter criteria.
Here, the filter of Slicer is copied as the basic filter.
Here, isRowHiddenByFilter() can be used for the created basic filter.
Delete the basic filter.
Usage
1. Create Slicer on Spreadsheet
Here, as a test case, please manually create new Slicer on a sheet in the Spreadsheet. This sample script supposes that only one Slicer is put in the active sheet.
2. IMPORTANT: Save Setting of Slicer
When Set current filters as default is selected (clicked), the setting of Slicer is saved. Please save the setting of the Slicer by selecting Set current filters as default. You can see Set current filters as default at the following figure.
If Set current filters as default is not selected, the setting of the current Slicer is not saved. Please be careful this. The official document can be seen at here.
3. Run sample script
When the function of myFunction() is run, the filtered values of Slicer on the active sheet can be retrieved using isRowHiddenByFilter().
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var slicer = sheet.getSlicers()[0];
var range = slicer.getRange();
var filter = range
.createFilter()
.setColumnFilterCriteria(
slicer.getColumnPosition(),
slicer.getFilterCriteria()
);
var result = range.getValues().filter(function(_, i) {
return !sheet.isRowHiddenByFilter(i + 1);
});
filter.remove();
Logger.log(result);
}
In this case, when return !sheet.isRowHiddenByFilter(i + 1); is modified to return sheet.isRowHiddenByFilter(i + 1);, the hidden rows can be retrieved.
References
isRowHiddenByFilter(rowPosition)
Class Slicer
Save your slicer filtering selections
This answer adds on #Tanaike's answer.
When there are multiple slicers on the page, you cannot create new filter per slicer but instead you have to apply new filter criterias to the existing filter on the page.
function filterFromSlicer() {
var sheet = SpreadsheetApp.getActiveSheet();
var slicers = sheet.getSlicers();
slicers.forEach(slicer => {
var range = slicer.getRange();
var filter = range.getFilter() || range.createFilter();
filter.setColumnFilterCriteria(
slicer.getColumnPosition(),
slicer.getFilterCriteria()
);
});
var result = sheet.getDataRange().getValues().filter(function (_, i) {
return !sheet.isRowHiddenByFilter(i + 1);
});
Logger.log(result);
}

Copying background color from one column to another with a custom function

I wrote this seemingly simple script to copy the background colors from one column to another.
When I run the script, I get no error messages, but nothing seems to happen.
This is what I type into a cell on the sheet:
=copyColor("B:B", "A:A")
I read a post on the Google Apps Script forum that implied that this type of procedure isn't possible, but I am determined to write a script that pulls it off.
Here is what the post said:
"As it is clearly explained in the documentation, Custom functions return values, but they cannot set values outside the cells they are in. In most circumstances, a custom function in cell A1 cannot modify cell A5. That is of course also true for other methods such as setBackground etc."
This is why I tried to get around the problem, by NOT USING the setBackgrounds() function.
Is there another way? Or is there a way I can fix mine to make it work?
function copyColor(rangeToCopy, rangeToPaste)
{
//an array to store the first background colors
var firstColors = [];
//an array to store the second background colors
var secondColors = [];
//this will assign the first range into a variable
var firstRange = SpreadsheetApp.getActiveSheet().getRange(rangeToCopy);
//this will store the colors of the range into the firstColors array
firstColors = firstRange.getBackgrounds();
//this will assign the second range to a variable
var secondRange = SpreadsheetApp.getActiveSheet().getRange(rangeToPaste);
//this will store the colors of the range into the secondColors array
secondColors = secondRange.getBackgrounds();
//compare the two color arrays. if they do not match, apply the first array to the second array
if (firstColors != secondColors)
{
secondColors = firstColors
}
}