Is it possible to track Active Cell change in Google Sheet Sidebar - google-apps-script

I'm writing a sidebar which displays additional information based on selected row/cell by user. It's rendering fine on sidebar open, but if the user changes active cell, I need to update content.
Apparently, I can add a "refresh" button in sidebar, but I really want to avoid clicking "refresh" every time. Putting it on timer also isn't very good cause will just spam with unnecessary requests to sheet app.
Has anyone ever did something similar and that approach did you use?
Maybe it's possible somehow to get event about user changing active cell into the sidebar javascript code?

I've put together a prototype of a sidebar that collects all the cell the user clicks in. Starting with an onSelectionChange() trigger to record the cells the user clicks in and recording them to PropertyService Document Properties, when the user moves the mouse over the sidebar the cells that were selected will show up.
First we have a simple Sidedbar
HTML_Sidebar.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<textarea id="textArea" rows="10" cols="35" onmouseenter="onMouseEnter()">
</textarea>
<?!= include('JS_Sidebar'); ?>
</body>
</html>
Next we have the client side code
JS_Sidebar.html
<script>
function onMouseEnter() {
try {
google.script.run.withSuccessHandler(
function(selections) {
let textArea = document.getElementById("textArea");
let text = textArea.value.trim();
selections.forEach( cell => {
text = text + "You clicked cell "+cell+"\n";
}
);
textArea.value = text;
}
).getLatestSelections();
}
catch(err) {
alert(err);
}
}
</script>
Now for all the server side code.
Code.gs
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function showSidebar() {
var html = HtmlService.createTemplateFromFile("HTML_Sidebar");
html = html.evaluate();
SpreadsheetApp.getUi().showSidebar(html);
}
function onSelectionChange(event) {
try {
// event = {"range":{"columnEnd":3,"columnStart":3,"rowEnd":1,"rowStart":1},"authMode":"LIMITED","source":{},"user":
// {"email":"xxxxxxxxxx#gmail.com","nickname":"xxxxxxxxx"}}
let properties = PropertiesService.getDocumentProperties();
let property = properties.getProperty("_Selections");
if( !property ) property = "";
property = property + JSON.stringify(event) + "\n";
properties.setProperty("_Selections",property);
}
catch(err) {
Logger.log("Error in onSelectionChange: "+err)
}
}
function getLatestSelections() {
try {
let properties = PropertiesService.getDocumentProperties();
let property = properties.getProperty("_Selections");
if( !property ) property = "";
properties.deleteProperty("_Selections");
properties = property.split("\n");
properties.pop(); // remove the last \n element
return properties.map( property => {
let cell = JSON.parse(property);
return getRangeA1(cell.range.rowStart,cell.range.columnStart);
}
);
}
catch(err) {
Logger.log("Error in getLatestSelections: "+err)
}
}
function getRangeA1(row,col) {
try {
let colNum = col;
let colName = "";
let modulo = 0;
while( colNum > 0 ) {
modulo = (colNum - 1) % 26;
colName = String.fromCharCode(65 + modulo) + colName;
colNum = Math.floor((colNum - modulo) / 26);
}
colName = colName+(row);
return colName;
}
catch(err) {
throw "Error in getRangeA1: "+err;
}
}
Screen shot

I understand that you want to record when the active cells change on a Sheet, and you want to do it without resorting to timers or buttons. If my understanding of your scenario is correct, then your best bet is using onSelectionChange triggers.
You can introduce an onSelectionChange trigger in your script to execute a refresh every time that the user changes the active cell. Leave a comment below if you have questions about this approach.

Related

finding Text with specific format and delete it

I have a big google doc file with over 100 pages(with tables etc) and there is some reference text in that document in multiple locations reference texts are highlighted with the color "grey", I want to have a function that can find those colors/style in the table or paragraph and delete it. So Step 1 is finding it, and then deleting(removing those texts from the document) it in one go.
How we did it in MS Word is, we created custom styles and assign those styles to those "Remarks Text"(in grey) and in VBA we look for text matching the style name, and if it returns true than we delete those texts. As much i know about doc, there is no option to create custom styles.
Here is the code I am trying:-
function removeText()
{
var doc = DocumentApp.getActiveDocument()
var body = doc.getBody()
body.getParagraphs().map(r=> {
if(r.getAttributes().BACKGROUND_COLOR === "#cccccc")
{
//Don't know what to do next, body.removeChild(r.getChild()) not working
}
})
}
Can you guide me on how I can achieve this effectively please.
Thanks
Try this
body.getParagraphs().forEach( r => {
if( r.getAttributes().BACKGROUND_COLOR === "#cccccc" ) {
r.removeFromParent();
}
}
Reference
Paragraph.removeFromParent()
Google Apps Script hasn't a method to find text based on their style attributes, instead we need to get each part and in order to be able to get their attributes. The following example, if the format is applied to the whole paragraph, it is deleted, if not, it uses the regular expression for finding any single character ..
function removeHighlightedText() {
// In case that we want to remove the hightlighting instead of deleting the content
const style = {};
style[DocumentApp.Attribute.BACKGROUND_COLOR] = null;
const backgroundColor = '#cccccc';
const doc = DocumentApp.getActiveDocument();
const searchPattern = '.';
let rangeElement = null;
const rangeElements = [];
doc.getParagraphs().forEach(paragraph => {
if (paragraph.getAttributes().BACKGROUND_COLOR === backgroundColor) {
paragraph.removeFromParent();
// Remove highlighting
// paragraph.setAttributes(style);
} else {
// Collect the rangeElements to be processed
while (rangeElement = paragraph.findText(searchPattern, rangeElement)) {
if (rangeElement != null && rangeElement.getStartOffset() != -1) {
const element = rangeElement.getElement();
if (element.getAttributes(rangeElement.getStartOffset()).BACKGROUND_COLOR === backgroundColor) {
rangeElements.push(rangeElement)
}
}
}
}
});
// Process the collected rangeElements in reverse order (makes things easier when deleting content)
rangeElements.reverse().forEach(r => {
if (r != null && r.getStartOffset() != -1) {
const element = r.getElement();
// Remove text
element.asText().deleteText(r.getStartOffset(), r.getEndOffsetInclusive())
// Remove highlighting
// element.setAttributes(textLocation.getStartOffset(), textLocation.getEndOffsetInclusive(), style);
}
});
}

Why does this Javascript function rewrite the innerHTML of one div but not the other?

I'm using NodeJS to query a MySQL database for a single entry of a journal, but the results aren't going to both of the assigned divs. I have an iFrame in my center column, dedicated to two divs (one hidden at any given time). One div is a read-only page for the journal entry, and the other one contains a TinyMCE rich-text editor. I have buttons in left column to switch between the views.
The rich-text editor loads properly on initial load of page, but doesn't update as I navigate with the calendar; the read-only innerHTML does update properly as I navigate.
calDt[] is an array that holds dates. calDt[0] is the active date, while calDt[1] holds a dummy date used for navigating the calendar without changing the entry.
app.js:
app.get('/getdata/:dateday', (req, res) => {
let sql = `SELECT entry FROM main where dateID ='${req.params.dateday}'`
let query = db.query(sql, (err, results) => {
if(err) {
throw err
}
res.send(JSON.stringify(results));
})
})
middle-left.ejs
<button style= "height:22px"; type="button" onclick="readDivHere()">Lock</button>
<button style= "height:22px"; type="button" onclick="editDivHere()">Edit</button></div>
<script> // the Lock button brings us back to the completed entry in the middle stuff
function readDivHere() {
document.getElementById('frame1').contentWindow.readDivHere();
document.getElementById('frame1').scrolling = "yes";
}
</script>
<script> // the Edit button will bring tinymce rich-text editor to the middle stuff
function editDivHere() {
document.getElementById('frame1').contentWindow.editDivHere();
document.getElementById('frame1').scrolling = "no";
}
</script>
middle-center.ejs
<iframe id="frame1" class="entryon" src="" frameborder="0px"></iframe>
<script>
document.getElementById("frame1").src = "iframe";
</script>
iframe.ejs
<div id="readDiv" class="here" style="display: block; background: white; padding-top: 0px; padding-left: 10px; padding-right: 8px; min-height: 810px; width: 967px;"><%- include ('entry'); %></div>
<div id="editDiv" class="here" style="display: none; padding: 0px;" ><%- include ('editPage'); %></div>
<script> //function that switches from rich-text editor back to real entry
function readDivHere() { // here we run a function to update text of read-only entry
document.getElementById("readDiv").style.display="block";
document.getElementById("editDiv").style.display="none";
}
</script>
<script> //function that switches from read-only entry to rich-text editor
function editDivHere() {
document.getElementById("readDiv").style.display="none";
document.getElementById("editDiv").style.display="block";
}
</script>
entry.ejs
<div id="readOnlyEntry"></div>
<script>
// load the active entry into the middle column for proper reading
function loadEntry(p) {
var x = parent.calDt[1].getFullYear();
var y = parent.calDt[1].getMonth();
y = y + 1;
if (y < 10) {
y = "0" + y;
};
if (p < 10) {
p = "0" + p;
}
var newDate = x + "" + y + "" + p; // p is a date formatted like 20210808
            var xhttp = new XMLHttpRequest();
            xhttp.onreadystatechange = function() {
                const text = this.responseText;
                const obj = JSON.parse(text);
document.getElementById("readOnlyEntry").innerHTML = obj[0].entry;
document.getElementById("richTextEd").innerHTML = obj[0].entry; // doesn't work!
            }
xhttp.open("GET", "../getdata/" + newDate, true);
        xhttp.send();
}
</script>
<script>
// rich-text editor populates correctly on load
loadEntry(parent.calDt[0].getDate());
</script>
editPage.ejs
<%- include ('tinymce'); %>
<form method="POST" action="../result" enctype="multipart/form-data">
<textarea name="content" id="richTextEd">Here's the default text.</textarea>
</form>
calendar-clicker.ejs
var p = x.innerHTML; // get the value of the calendar cell text (i.e. day of the month)
p = p.match(/\>(.*)\</)[1];
var d = calDt[1].getFullYear(); // what year is the calendar referencing?
var q = calDt[1].getMonth(); // what month is the calendar referencing?
q = q + 1; // compensate for javascript's weird month offset
calDt[0] = new Date(q + "/" + p + "/" + d); // assign a new global date variable
calDt[1] = calDt[0]; // temporarily reset navigation date to active date
document.getElementById('frame1').contentWindow.loadEntry(p);
Does the failure have to do with assigning the innerHTML to a different .ejs? If I put the form into the same div as the read-only entry, the form still fails to update as I navigate.
Solved it.
In entry.ejs, I replaced...
document.getElementById("richTextEd").innerHTML = obj[0].entry;
with
tinymce.get("richTextEd").setContent(obj[0].entry);
https://www.tiny.cloud/blog/how-to-get-content-and-set-content-in-tinymce/

Scrape site to report css selector occurrence in HTML

I want to see how much of my team's code has been integrated into a large scale site.
I believe I can achieve this (albeit roughly), by getting statistics on the number of occurrences certain CSS selectors appear across all the HTML pages. I have some unique CSS class selectors that I would like to use when scraping the site to analyze:
On how many pages the selector occurs.
On any page it does, how many times.
I've looked around but can't find any tools - does anyone know of any, or could suggest any idea's that may help me quickly achieve this ?
Thanks in advance.
Thanks to everyone for their advice.
In the end I decided that there was no one tool that could help me gather the statistics in the way I described so I already started to build up the application I needed in Node. Although I've not used Node before I've found it quick to grasp with an intermediate knowledge of Javascript.
For anyone looking to do the same:
I've used Simplecrawler to run over the site and Cheerio to find selectors and from this I can create a simple report created in Json using FS.
I'd recommend you to use Google App Scripting. You might manage to crawl site's pages and count the CSS selector occurrences with regex. Modify he following code to search each page for CSS selector. The code explanation is here.
Code
function onOpen() {
DocumentApp.getUi() // Or DocumentApp or FormApp.
.createMenu('New scrape web docs')
.addItem('Enter Url', 'showPrompt')
.addToUi();
}
function showPrompt() {
var ui = DocumentApp.getUi();
var result = ui.prompt(
'Scrape whole website into text!',
'Please enter website url (with http(s)://):',
ui.ButtonSet.OK_CANCEL);
// Process the user's response.
var button = result.getSelectedButton();
var url = result.getResponseText();
var links=[];
var base_url = url;
if (button == ui.Button.OK) { // User clicked "OK".
if(!isValidURL(url))
{
ui.alert('Your url is not valid.');
}
else {
// gather initial links
var inner_links_arr = scrapeAndPaste(url, 1); // first run and clear the document
links = links.concat(inner_links_arr); // append an array to all the links
var new_links=[]; // array for new links
var processed_urls =[url]; // processed links
var link, current;
while (links.length)
{
link = links.shift(); // get the most left link (inner url)
processed_urls.push(link);
current = base_url + link;
new_links = scrapeAndPaste(current, 0); // second and consecutive runs we do not clear up the document
//ui.alert('Processed... ' + current + '\nReturned links: ' + new_links.join('\n') );
// add new links into links array (stack) if appropriate
for (var i in new_links){
var item = new_links[i];
if (links.indexOf(item) === -1 && processed_urls.indexOf(item) === -1)
links.push(item);
}
/* // alert message for debugging
ui.alert('Links in stack: ' + links.join(' ')
+ '\nTotal links in stack: ' + links.length
+ '\nProcessed: ' + processed_urls.join(' ')
+ '\nTotal processed: ' + processed_urls.length);
*/
}
}
}
}
function scrapeAndPaste(url, clear) {
var text;
try {
var html = UrlFetchApp.fetch(url).getContentText();
// some html pre-processing
if (html.indexOf('</head>') !== -1 ){
html = html.split('</head>')[1];
}
if (html.indexOf('</body>') !== -1 ){ // thus we split the body only
html = html.split('</body>')[0] + '</body>';
}
// fetch inner links
var inner_links_arr= [];
var linkRegExp = /href="(.*?)"/gi; // regex expression object
var match = linkRegExp.exec(html);
while (match != null) {
// matched text: match[0]
if (match[1].indexOf('#') !== 0
&& match[1].indexOf('http') !== 0
//&& match[1].indexOf('https://') !== 0
&& match[1].indexOf('mailto:') !== 0
&& match[1].indexOf('.pdf') === -1 ) {
inner_links_arr.push(match[1]);
}
// match start: match.index
// capturing group n: match[n]
match = linkRegExp.exec(html);
}
text = getTextFromHtml(html);
outputText(url, text, clear); // output text into the current document with given url
return inner_links_arr; //we return all inner links of this doc as array
} catch (e) {
MailApp.sendEmail(Session.getActiveUser().getEmail(), "Scrape error report at "
+ Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd HH:mm:ss"),
"\r\nMessage: " + e.message
+ "\r\nFile: " + e.fileName+ '.gs'
+ "\r\nWeb page under scrape: " + url
+ "\r\nLine: " + e.lineNumber);
outputText(url, 'Scrape error for this page cause of malformed html!', clear);
}
}
function getTextFromHtml(html) {
return getTextFromNode(Xml.parse(html, true).getElement());
}
function getTextFromNode(x) {
switch(x.toString()) {
case 'XmlText': return x.toXmlString();
case 'XmlElement': return x.getNodes().map(getTextFromNode).join(' ');
default: return '';
}
}
function outputText(url, text, clear){
var body = DocumentApp.getActiveDocument().getBody();
if (clear){
body.clear();
}
else {
body.appendHorizontalRule();
}
var section = body.appendParagraph(' * ' + url);
section.setHeading(DocumentApp.ParagraphHeading.HEADING2);
body.appendParagraph(text);
}
function isValidURL(url){
var RegExp = /^(([\w]+:)?\/\/)?(([\d\w]|%[a-fA-f\d]{2,2})+(:([\d\w]|%[a-fA-f\d]{2,2})+)?#)?([\d\w][-\d\w]{0,253}[\d\w]\.)+[\w]{2,4}(:[\d]+)?(\/([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)*(\?(&?([-+_~.\d\w]|%[a-fA-f\d]{2,2})=?)*)?(#([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)?$/;
if(RegExp.test(url)){
return true;
}else{
return false;
}
}

Google Apps Script: How to remove empty Gmail labels?

With Google Apps Script, is it possible to remove empty (unused) Gmail labels?
Based on the answers above, here is a Google Apps Script to delete empty labels (with nested labels check). The Javascript is rough, but it works! The 'testing' variable determines if it just logs or actually deletes the labels.
You can debug, run Google Apps Scripts at https://script.google.com
//
// Set to 'false' if you want to actually delete labels
// otherwise it will log them but not delete them.
//
var testing = true;
//
// Deletes labels with no email threads
//
function deleteEmptyLabels() {
Logger.log("Starting label cleanup");
var allLabels = GmailApp.getUserLabels();
var emptyLabels = allLabels.filter(function(label){ return isTreeEmpty(label, allLabels); } );
for (var i = 0; i < emptyLabels.length; i++){
Logger.log('Deleting empty label ' + emptyLabels[i].getName());
if (!testing){
emptyLabels[i].deleteLabel();
}
}
Logger.log("Finished label cleanup");
}
//
// Finds labels below a parent
//
function getNestedLabels(parent, allLabels) {
var name = parent.getName() + '/';
return allLabels.filter(function(label) {
return label.getName().slice(0, name.length) == name;
});
}
//
// Tests a single label for 'emptiness'
//
function isLabelEmpty(label){
return label.getThreads(0, 1) == 0;
}
//
// Tests a label, and nested labels for 'emptiness'
//
function isTreeEmpty(label, allLabels){
if (!isLabelEmpty(label))
return false;
var nested = getNestedLabels(label, allLabels);
for(var j = 0; j < nested.length; j++){
if (!isTreeEmpty(nested[j], allLabels))
return false;
}
return true;
}
Certainly, first use GmailApp.getUserLabels() to retrieve all the labels, then loop over them and use getThreads() to determine if a given label is empty, and finally use deleteLabel() to remove empty ones.
See:
https://developers.google.com/apps-script/reference/gmail/gmail-app
https://developers.google.com/apps-script/reference/gmail/gmail-label
GmailApp.getUserLabels(), getThreads() and deleteLabel() is the way to go, but take care not to delete empty labels if one of its sub-labels is not!

How to Read a text file using Java script and display it in column fashion in HTML.?

This is the code which reads the text file using the Jscript and displays it in HTML. But i need it to display it in table.
How to display it in Table.
< this is my first question so i hope i get solution >
`
Read File (via User Input selection)
var reader; //GLOBAL File Reader object for demo purpose only
/**
* Check for the various File API support.
*/
function checkFileAPI() {
if (window.File && window.FileReader && window.FileList && window.Blob) {
reader = new FileReader();
return true;
} else {
alert('The File APIs are not fully supported by your browser. Fallback required.');
return false;
}
}
/**
* read text input
*/
function readText(filePath) {
var output = ""; //placeholder for text output
if(filePath.files && filePath.files[0]) {
reader.onload = function (e) {
output = e.target.result;
displayContents(output);
};//end onload()
reader.readAsText(filePath.files[0]);
}//end if html5 filelist support
else if(ActiveXObject && filePath) { //fallback to IE 6-8 support via ActiveX
try {
reader = new ActiveXObject("Scripting.FileSystemObject");
var file = reader.OpenTextFile(filePath, 1); //ActiveX File Object
output = file.ReadAll(); //text contents of file
file.Close(); //close file "input stream"
displayContents(output);
} catch (e) {
if (e.number == -2146827859) {
alert('Unable to access local files due to browser security settings. ' +
'To overcome this, go to Tools->Internet Options->Security->Custom Level. ' +
'Find the setting for "Initialize and script ActiveX controls not marked as safe" and change it to "Enable" or "Prompt"');
}
}
}
else { //this is where you could fallback to Java Applet, Flash or similar
return false;
}
return true;
}
/**
* display content using a basic HTML replacement
*/
function displayContents(txt) {
var el = document.getElementById('main');
el.innerHTML = txt; //display output in DOM
}
</script>
</head>
<body onload="checkFileAPI();">
<div id="container">
<input type="file" onchange='readText(this)' />
<br/>
<hr/>
<h3>Contents of the Text file:</h3>
<div id="main">
...
</div>
</div>
</body>
</html>`
Please help me in this
You could format the text file like a SSV (TSV or CSV as well), then instead of ReadAll() I'd do something like this:
var file = reader.OpenTextFile(filePath, 1),
data = [], n;
while (!file.AtEndOfStream) {
data.push(file.ReadLine().split(';')); // or use some other "cell-separator"
}
Then the rest is a lot simpler and faster, if you've an empty table element in your HTML:
<table id="table"></table>
Now just create rows and cells dynamically based on the data array:
var table = document.getElementById('table'),
len = data.length,
r, row, c, cell;
for (r = 0; r < len; r++) {
row = table.insertRow(-1);
for (c = 0; c < data[r].lenght; r++) {
cell.row.insertCell(-1);
cell.innerHTML = data[r][c];
}
}