I am building an automated solution on Google Apps Scripts for my job that is testing my basic knowledge of HTML and JS. I would appreciate some guidance.
Context:
I am building a tool to automate certain tasks on Google Sheets through formulas and scripts
The task I'm having issues with begins with the user clicking on a button that opens a getUi() Modal Dialog
The main challenge is to extract data out of an HTLM table where the user makes inputs
JS code triggering the popup (just for context):
function openbrief() {
//pull necessary values
var supp_main = SpreadsheetApp.getActive().getSheetByName('supp_main')
//projects
var projrng = supp_main.getRange(2, 64, 1000);
var projval = projrng.getValues();
var projname = projval.filter(String)
//content_category
var catrng = supp_main.getRange(2, 65, 1000);
var catval = catrng.getValues();
var catname = catval.filter(String)
//content_type
var typerng = supp_main.getRange(2, 66, 1000);
var typeval = typerng.getValues();
var typename = typeval.filter(String)
//format
var formatrng = supp_main.getRange(2, 67, 1000);
var formatval = formatrng.getValues();
var formatname = formatval.filter(String)
//channel
var chanrng = supp_main.getRange(2, 68, 1000);
var chanval = chanrng.getValues();
var channame = chanval.filter(String)
var widget = HtmlService.createTemplateFromFile('briefs');
widget.projdata = projname.reduce((s, [e]) => s += `<option value="${e}">${e}<\/option>\n`, "");
widget.catdata = catname.reduce((s, [e]) => s += `<option value="${e}">${e}<\/option>\n`, "");
widget.typedata = typename.reduce((s, [e]) => s += `<option value="${e}">${e}<\/option>\n`, "");
widget.formatdata = formatname.reduce((s, [e]) => s += `<option value="${e}">${e}<\/option>\n`, "");
widget.chandata = channame.reduce((s, [e]) => s += `<option value="${e}">${e}<\/option>\n`, "");
SpreadsheetApp.getUi().showModalDialog(widget.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setHeight(5000).setWidth(5000), 'Briefs');
}
Questions:
I have a GS/JS script within the HTML that is supposed to run through the table values, push them to an array and paste them to a gSheet. However it does not do that. I'm not experience with for loops and arrays on JS, and I can't tell what's not working.
Will this type of script pull the values from the dropdown menus? I suspect it won't as they seem to be sitting on top of the cell. But I'm unsure (see screenshot for more context)
Any other improvements or suggestions based on both code or screenshot is very welcome
References:
The script is "inspired" by this: Getting value from table cell in JavaScript...not jQuery
Here's another case I've been browsing but I fail to understand the solution provided: extract value from dynamic html table
HTML code
<table id="brief_table">
<thead>
<tr>
<th>Content category</th>
<th>Description</th>
<th>Content type</th>
<th>Format</th>
<th>Channel</th>
<th>Assets</th>
<th>Start date</th>
<th>End date</th>
<th>Relevant links</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select id="catname" style="Border: none;"><?!= catdata ?></select>
</td>
<td contenteditable='true'>Test</td>
<td>
<select id="typename" style="Border: none;"><?!= typedata ?></select>
</td>
<td>
<select id="formatname" style="Border: none;"><?!= formatdata ?></select>
</td>
<td>
<select id="channame" style="Border: none;"><?!= chandata ?></select>
</td>
<td contenteditable='true'>Test</td>
<td contenteditable='true'>Test</td>
<td contenteditable='true'>Test</td>
<td contenteditable='true'>Test</td>
<td contenteditable='true'>Test</td>
</tr>
</tbody>
</table>
<script>
function pasteBrief() {
var table = document.getElementById("brief_table");
var tablevalues = [];
for (var r = 0, n = table.rows.length; r < n; r++) {
for (var c = 0, m = table.rows[r].cells.length; c < m; c++) {
tablevalues.push(table.rows[r].cells[c].innerHTML);
}
}
alert(tablevalues);
google.script.run.pasteBriefVal(tablevalues);
}
</script>
GS SERVER CODE
function pasteBriefVal(tablevalues){
SpreadsheetApp.getActive().getSheetByName('supp_main').getRange('bs1:cb').setValues(tablevalues);
}
Screenshots:
Additional info:
I have managed to stringify the array, which in return allow me to paste it as a value in one single cell. This enable me to take a closer look to it (see img below)
Link to copy of the spreadsheet: link - to trigger the popup "briefs.html" you want to go to "[SOW] #2-Project & deliverables" and click on the icon that says "PLAN" around D24
Edits:
Moved setValues out of HTML file back to GS side. Called the new GS function out from HTML via google.script.run.pasteBriefVal(tablevalues)
Added more information on what the array look like
Added the link to a copy of the original spreadsheet should anyone want to take a deeper look
Server side GS and Client side JS
The below server side code does not run in a browser:
SpreadsheetApp.getActive().getSheetByName('supp_main').getRange('bs1').setValues(tablevalues);
If you wish to run server side code the use google.script.run
Related
i have a site where i paste my entire source code into a box and update all the td tags with a background color if there isnt currently a "bgcolor" attribute.
I've been messing with this for some time but i can't get my ogSource to update. I've tried many ways such as assigning new variables, returns etc etc. No luck.
the below code properly scans for the appropriate td and adds the background color, it just doesnt apply it to the ogSource. I've removed all my other code to make this as basic as possible.
Can anyone assist with this?
Thanks in advance.
var ogSource = '<table id="test1"> <tr> <td> <table id="test2"> <tr> <td> </td> </tr> </table></td> </tr> </table>'
ogSource.replace(/\<td(.*?)\>/g, function(matches) {
if (!matches.includes('bgcolor')) {
var idx = matches.lastIndexOf(">");
if (idx > -1) {
matches = matches.substr(0, idx) + " bgcolor='pink'" + matches.substr(idx);
}
}
});
console.log(ogSource);
EDIT/UPDATE
After a lot of messing around- this was a solution that was able to capture all the source code pasted and make the modification needed.
ogSource = ogSource.replace(/\<td(.*?)\>/g, function( matches , i ) {
var idx = matches.lastIndexOf(">");
if (idx > -1) {
if (!matches.includes('bgcolor')) {
ogSource = matches.substr(0, idx) + " bgcolor='pink'" + matches.substr(idx);
} else {
ogSource = matches;
}
} return ogSource;
});
console.log(ogSource);
My initial answer was off the mark but quite a bit, however, I think regex in general may not be the best solution due to the amount of edge cases present and the DOMParser might be a better solution for this.
Essentially, you pass the html string into the DOMParser method parseFromString and store that in a variable, then select all td elements and check if they have a bgColor attribute, if they don't, give them one, then output the new DOM string.
Here's an example:
const domParser = new DOMParser();
const DOM = domParser.parseFromString(`<table id="test1"> <tr> <td> <table id="test2"> <tr> <td> </td> </tr> </table></td> </tr> </table>`, "text/html");
// Find all tds
const tds = DOM.querySelectorAll("td");
for(let i = 0; i < tds.length; i++) {
let currentTD = tds[i];
if(!currentTD.hasAttribute("bgColor")) {
currentTD.setAttribute("bgColor", "someValue");
}
}
console.log(DOM.body.innerHTML); // If you only want to return the table content
console.log(DOM.querySelector("html").innerHTML); // If you want all of the html code that was added
I want to add numbers in <td></td> below via JavaScript. For example using the following description:
<td id='last'> + formatNumber(data.tickers[key].last) + </td>
<td id='high'> + formatNumber(data.tickers[key].high) + </td>
<td id='low'> + formatNumber(data.tickers[key].low) + </td>
How do I change the text of table data elements via JavaScript?
<td id='new1'> = + <td id='last'> + <td id='high'> + </td>
<td id='new2'> = + <td id='high'> + <td id='loww'> + </td>
Try this:
// these target the cell elements
let last = document.getElementById("last");
let high = document.getElementById("high");
let low = document.getElementById("low");
let new1 = document.getElementById("new1");
let new2 = document.getElementById("new2");
// now we convert cell content to numbers, add them and make them 2 decimal places.
new1.textContent = (parseFloat(last.textContent) + parseFloat(high.textContent)).toFixed(2);
new2.textContent = (parseFloat(high.textContent) + parseFloat(low.textContent)).toFixed(2);
td {
border: solid 1px;
}
<table>
<tr>
<th>last</th>
<th>high</th>
<th>low</th>
<th>new1</th>
<th>new2</th>
</tr>
<tr>
<td id='last'> 23.40 </td>
<td id='high'> 28.20 </td>
<td id='low'> 22.10 </td>
<td id='new1'></td>
<td id='new2'></td>
</tr>
</table>
First I'm going to make your life a bit easier. Instead of using document.getElementsByTagName('tr')[3].getElementsByTagName('td')[2] to get the fourth tr element's third td element ([0] = first, [2] = third, etc) this will help make your code much much easier to read. You don't need id attributes on every element if you know how reliable code and order are by default.
function tag_(t)
{//Example: tag_('body')[0];
return (document.getElementsByTagName(t)) ? document.getElementsByTagName(t) : false;
}
Object.prototype.tag_ = function(t)
{//Example: id_('unique_id').tag_('td')[0];
return (this.getElementsByTagName && this.getElementsByTagName(t) && this.getElementsByTagName(t).length > 0) ? this.getElementsByTagName(t) : false;
}
Secondly the easiest way to both read and write data to any element is to use textContent.
Read the fourth td on the third tr:
//Read an element's text node:
console.log(tag_('tr')[2].tag_('td')[5].textContent);
//Write to an element's text node:
tag_('table')[0].tag_('tr')[2].tag_('td')[5].textContent = '1,234');
JavaScript is a bit strict when it comes to types. So if you need to do some math with text content that you just read you need to convert it:
Number(tag_('tr')[1].tag_('td')[5].textContent);//'123' becomes `123`
Number(tag_('tr')[2].tag_('td')[2].textContent);//'a123' becomes `NaN` (Not a Number)
If I recall correctly I recently used the following to strip non-numeric text from a string:
var my_number = Number('String or replace with object reference'.replace(/\D/g,''));
Now that you're getting the read/write aspects and overcoming some of the more oddities associated with it I'll iterate over...iteration! You may already know this though I'm presuming a full answer is more desirable than a partial answer for not just you though also others reading this in the future.
var table = tag_('table');
for (var i = 0; i < table.length; i++)
{
console.log(table[i]);
var tr = table[i].tag_('tr');//Whatever table[i] is and it's table rows.
for (var j = 0; j < tr[i].length; j++)
{
console.log(tr[j]);
var td = table[i].tag_('tr')[j].tag_('td');//All the table data elements.
for (var k = 0; k < td.length; k++)
{
//apply read/write conditions here.
//potentially call a second global function to keep your code reusable.
}
}
}
That should help you get far enough with specific and iteral targeting of table data elements to help you learn and achieve your goals.
I've run with a problem with my web app.
Here's my code:
#app.route('/addrec',methods = ['POST', 'GET'])
def addrec():
if g.user:
if request.method == 'POST':
#UPPER PANE
payor = request.form['payor']
receiptno = request.form['OR']
paymentmethod = request.form['paymentmethod']
naive_dt = time.strftime("%m/%d/%Y")
collectiondate = naive_dt = datetime.now()
message = request.form['message']
#LOWER PANE
url_to_scrape = 'http://localhost:5000/form'
r = requests.get(url_to_scrape)
soup = BeautifulSoup(r.text)
nature = []
for table_row in soup.select("table.inmatesList tr"):
cells = table_row.findAll('td')
if len(cells) > 0:
nature = cells[0].text.strip()
natureamt = cells[1].text.strip()
nature = {'nature': nature, 'nature': natureamt}
nature_list.append(nature)
ent = Entry(receiptno, payor,officer, paymentmethod, collectiondate,message, nature_list)
add_entry(ent)
actions="Applied"
return redirect(url_for('form'))
return redirect(url_for('home'))
As you can see I am getting each of the values from my forms and is scraping the values in my table using beautifulsoup. However after I click the submit button, it loads forever. I am getting the valeus from the upper pane but not in the table.
By the way I am generating my cells from a javascript function onClick. Just in case my javascript might be the problem. or maybe there's an easy way to extract these values from the javascrip functions -> python. Here's my javascript code and HTML
<script type="text/javascript">
function deleteRow(o){
var p=o.parentNode.parentNode;
p.parentNode.removeChild(p);
}
function addRow()
{
var table = document.getElementById("datatable"),
newRow = table.insertRow(table.length),
cell1 = newRow.insertCell(0),
cell2 = newRow.insertCell(1),
cell3 = newRow.insertCell(2),
name = document.getElementById("form").value,
amount = document.getElementById("amount").value;
delete1 = delete1 = '<input type="button" class="btn btn-danger" class="glyphicon glyphicon-trash"id="delete" value="Delete" onclick="deleteRow(this)">';
cell1.innerHTML = name;
cell2.innerHTML = amount;
cell3.innerHTML = delete1;
findTotal();
}
function findTotal(){
var arr = document.querySelectorAll("#datatable td:nth-child(2)");
var tot=0;
for(var i=0;i<arr.length;i++){
var enter_value = Number(arr[i].textContent)
if(enter_value)
tot += Number(arr[i].textContent);
}
document.getElementById('total').value = tot;
}
</script>
HTML:
<form name="noc">
<input class="form-control input-lg" id="form" list="languages" placeholder="Search" type="text" required>
<br>
<input class="form-control input-lg" id="amount" list="languages" placeholder="Amount" type="number" required>
<br>
<button onclick="addRow(); return false;">Add Item</button>
</form>
<table id="datatable" class="table table-striped table-bordered" cellspacing="0" width="100%">
<thead>
<tr>
<th>Nature of Collection</th>
<th>Amount</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
</tr>
</tbody>
</table>
The data of these scraped values, I expect them to be saved to my database. On a cell. If possible I would like the list to be inserted in a column so I can get them later.
Or is there a way I can get the lists on a cleaner and better way to my database? Any help is appreciated. Thank you!
So it looks like you're using requests to try and get data generated by JS. Well this isn't going to work, unless you know some magic a lot of people don't. Requests can't deal with the JS, so it never runs. You should be able to get the data using selenium or something to automate a browser. Otherwise, I don't think you're going to be able to scrape it like this. But if someone knows a way to get JS generated data with requests, please post it.
https://jsfiddle.net/bob90937/2yw3s376/
I am able to one sentence in one content as u can see from the following
jsp,html:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<p id="sentence">a paragraph</p>
<p id="content">And this here is inside a paragraph , about paragliders. happy ending.</p>
css:
.highlighted {
background: yellow
}
javascript:
$(document).ready(function() {
var wordText = $('#sentence').text();
var rootText = $('#content').text();
var rootNode = document.getElementById('content');
highlightWord(rootText, wordText);
function highlightWord(rootText, wordText) {
textNodesUnder(rootNode).forEach(highlightWords);
function textNodesUnder(rootNode) {
var walk = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT, null, false);
var text = [],
node;
while (node = walk.nextNode()) text.push(node);
return text;
}
function highlightWords(n) {
for (var i;
(i = n.nodeValue.indexOf(wordText, i)) > -1; n = after) {
var after = n.splitText(i + wordText.length);
var highlighted = n.splitText(i);
var span = document.createElement('span');
span.className = 'highlighted';
span.appendChild(highlighted);
after.parentNode.insertBefore(span, after);
}
}
}
});
IF i have multiple sentence and content and I save it in the table.
How to change the code so that the multiple sentence and content can be highlighted???
..
<table class="gridtable" border="1">
<tbody>
<tr>
<th>sentence</th>
<th>content</th>
</tr>
<tr>
<td id="sentence">a paragraph</td>
<td id="content">And this here is inside a paragraph , about paragliders.happy ending.</td>
<tr>
<td id="sentence">unless they were granted</td>
<td id="content">All Singaporean children of school-going age are required to regularly attend a national primary school, unless they were granted an exemption due to physical or intellectual disabilities.</td>
<tr>
<td id="sentence">efforts to</td>
<td id="content">MOE said that the change is part of the Government ongoing efforts to build a more inclusive society.</td>
</tr>
</table>
https://jsfiddle.net/bob90937/2yw3s376/ the link
You have an ID clash. You gave multiple elements ID's of content and sentence. Your Javascript is only retrieving the first one it finds.
I'm not sure if I'm trying to do too much here, but here is the scenario. I have an asp.net mvc page that, on the first time loading, returns a table of data in a view using the standard foreach mechanisms in the mvc framework. If the user has javascript enabled, I want to use knockout to update the table going forward. Is there a way to have knockout read the data from the dom table and use that data as the initial observable collection. From then on out, I would use knockout and ajax to add, edit, or delete data.
In a nutshell, I need to parse an html table into a knockout observable collection.
I've had a go at coding this up:
Here's the basic markup:
<table id="table" data-bind="template: { name: 'table-template' }">
<thead>
<tr>
<th>Name</th>
<th>Surname</th>
</tr>
</thead>
<tbody>
<tr>
<td>Richard</td>
<td>Willis</td>
</tr>
<tr>
<td>John</td>
<td>Smith</td>
</tr>
</tbody>
</table>
<!-- Here is the template we'll use for re-building the table -->
<script type="text/html" id="table-template">
<thead>
<tr>
<th>Name</th>
<th>Surname</th>
</tr>
</thead>
<tbody data-bind="foreach: data">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: surname"></td>
</tr>
</tbody>
</script>
Javascript:
(function() {
function getTableData() {
// http://johndyer.name/html-table-to-json/
var table = document.getElementById('table');
var data = [];
var headers = [];
for (var i = 0; i < table.rows[0].cells.length; i++) {
headers[i] = table.rows[0].cells[i].innerHTML.toLowerCase().replace(/ /gi, '');
}
// go through cells
for (var i = 1; i < table.rows.length; i++) {
var tableRow = table.rows[i];
var rowData = {};
for (var j = 0; j < tableRow.cells.length; j++) {
rowData[headers[j]] = tableRow.cells[j].innerHTML;
}
data.push(rowData);
}
return data;
}
var Vm = function () {
this.data = ko.observableArray(getTableData());
};
ko.applyBindings(new Vm(), document.getElementById('table'));
})();
You can extend this concept using the mapping plugin to create observables for each row: http://knockoutjs.com/documentation/plugins-mapping.html
View a demo here: http://jsfiddle.net/CShqK/1/
EDIT: I'm not saying this is the best approach, as it can be costly to traverse a large table to get the data. I would probably just output the JSON in the page as suggested by others in this thread.
How about just feeding your observable array with the data instead of parsing the html table
myArray: ko.observableArray(#Html.Raw(Json.Encode(Model.myTableData)))
If you really need to go the parsing the html way, you can use the following code
var tableData = $('#myTable tr').map(function(){
return [
$('td,th',this).map(function(){
return $(this).text();
}).get()
];
}).get();
$(document).ready(function() {
var myData = JSON.stringify(tableData);
alert(myData)
});
Here is a fiddle showing the code in action:
http://jsfiddle.net/FWCXH/