Find out when a row has been edited by user - tabulator

I need to know when a row edit ended, (this means when cell editor moved to another row or cell editor was hidden with any changes), to send edited data to service. I have cellEditing and cellEdited events, but sending data after each cellEdited is too expensive.
May be I can use, for example, cellEdited with some additional checks, but I can't figure out how. Or may be there is a better way to do it?

You can call the isEdited on a cell component to see if it has been edited in the past, so in the cellEdited callback you could cycle over the cells in the row to ses if they have all been edited:
dataEdited:function(cell){
var cells = cell.getRow().getCells();
var allEdited = true;
cells.forEach(function(cell){
if(!cell.isEdited()){
allEdited = false;
}
});
if(allEdited){
//do something when all cells have been edited
}
}
you can also cleare the edited flag on a cell using the 'clearEdited' function on the cell component, this could be useful to allow the user to edit again after you have performed whatever you need to after the row edit:
cell.clearEdited();

I found a temporary solution for now.
First of all, I'am catching cellEdited, store current cell and starting a timer. This needs to define, if some editor will shown after current one. If timer is completed, I realize that row edition is ended and do my job.
Then I'am catching cellEditing, to check if row editing is continued. If cellEdition is fired, check timer existence, if it is, stop the timer and check: if current cell is in row with last edited cell, if it is, do nothing (row editing continues), if it is not, do my job (last row edit ended).
Here is part of my Angular controller with code:
editTimerId?: any;
lastEditedCell: Tabulator.CellComponent;
cellEditCompleted(cell: Tabulator.CellComponent) {
this.lastEditedCell = cell;
this.editTimerId = setTimeout(() => this.timerComplete(), 2000);
}
timerComplete() {
// Do the thing
}
cellEditingBegin(cell: Tabulator.CellComponent) {
if (this.editTimerId) {
clearTimeout(this.editTimerId);
this.editTimerId = null;
let lastRow = this.lastEditedCell.getRow();
if (cell.getRow() !== lastRow) {
// Do the thing
}
}
}
I think this solution is too complicated, so if someone have a better way, I'll be glad to see it.

Related

Excel custom functions show BUSY! but async function is not called

I have an Excel Add-in having custom functions and taskpane. My client created a workbook having multiple sheets using my custom functions approximately 3500+ function calls in current workbook. When a user opens this workbook, I need to recalculate sheet so that only my functions are recalculated. To achieve this task, I have performed following steps.
Loop on sheets in workbook.
Search each sheet for my formula using worksheet.findAllOrNullObject() function.
if search result is not NullObject, then call ranges.calculate(). Which should trigger function calls.
var sheets = context.workbook.worksheets;
sheets.load("items/name");
await context.sync();
for (var i = 0; i < sheets.items.length; i++) {
var sheet = sheets.items[i];
const foundRanges = sheet.findAllOrNullObject(FORMULA_DATA[formula], {
completeMatch: false,
matchCase: false
});
await context.sync();
if (!foundRanges.isNullObject)
foundRanges.calculate();
await context.sync();
}
}
Problem I am facing that when I call recalculate function, all cells referring to these functions show BUSY! which means that my function have not resolved promise yet, but no function is actually called. I added break points at start of each function during debugging but no code stops there (I a change a single cell then breakpoint is hit).
I enabled run-time logging and it has entries for each call begin but no end call entry.
Also one of the cell reference is passed to all functions and if I change its value, then all function calls are made properly and it shows result as desired and logfile contains entries for begin and end for all calls.
After thoroughly investigating the issue, I have come to conclusion that this client was using other Add-in which had same function names and it was saved in workbook. When I recalculated the sheet range areas, it was trying to call functions from those old Add-in which was uninstalled earlier. Therefore all cells were showing BUSY! and since code was not there in excel these promises were never resolved. Once I removed all taskpanes from workbook and re-opened it this problem does not appeared again.
To save Taskpanes info
Office.addin.setStartupBehavior(Office.StartupBehavior.load);
To Remove Taskpanes information, use File => Info => Check for Issues => Inspect Document => Inspect.
After it displays results , Task pane Add-ins => Remove All.

Unlock individual cells before protect sheet

There's another question addressing how to protect the sheet but doesn't show how to unlock individual cells.
I can't just start recording & create a script (which usually show how it's done) because apparently, excel online still doesn't support protecting sheets from the web UI.
This took me a while to find out, so I'm answering my own question here for the benefit of others with the same need.
function main(workbook: ExcelScript.Workbook) {
let sheet = workbook.getWorksheet("Sheet1");
// will not let you lock/unlock cells if protected
sheet.getProtection().unprotect();
// just checking
let locked = sheet.getRange("B9").getFormat().getProtection().getLocked()
console.log('value of locked: ', locked)
// unlock
sheet.getRange("B9").getFormat().getProtection().setLocked(false)
// sanity check
locked = sheet.getRange("B9").getFormat().getProtection().getLocked()
console.log('locked value after set to false: ',locked)
sheet.getProtection().protect({
allowFormatCells: false
//allowFormatCells: true
});
}

Is there a google-script method like Application.Intersect(Target, Range) in Excel?

Now I understand that the question is deeper, and is connected with the tracking of events.
In Excel, I use this code:
If Not Intersect(Target, Sh.Range("$A$1:$A$300")) Is Nothing sub_do_something()
Here, Target - the address of the selected cell, Intersect determines whether the cell belongs to the specified range.
I use it in the system for filling and calculating the costing of the project.
The user clicks a row in a specific section of the calculation template. The script determines the address of the selected cell and switches the user to a specific sheet of the directory. Next, the user clicks on the desired line of the directory, the script copies a certain range of cells in the line and returns the user back to the calculation. When this happens, the copied data is inserted into a range of cells, starting with the selected one.
Thus, the creating a calculation, in which there can be more than 100 positions, is greatly simplified.
In Excel, everything works fine, but soon I plan to transfer this project to a cloud-based service, and Google Sheets is the best option.
Alas, only some events can be tracked in GAS, for example, using onOpen or onEdit triggers.
Excel has much more tracked events.
After a search on the StackOverflow, I found several similar issues related to tracing events, for example, How to find where user's cursor is in Document-bound script, Can we implement some code that fires upon selecting something in google document?, Google app script monitor spreadsheet selected ranges.
From the answers to these questions, it is clear that in GAS there is no such simple solution as Intersect(Target, Range) in Excel.
The last example uses the side menu, running a script from it that queries the sheet 5 times per second, and displays the address of the active cell in the "data" field.
Unfortunately, this code does not work for me. In the debugger, the getActiveRange() function works fine, but this code does not work:
$(document).ready(() => {
setInterval(()=>{
google.script.run.withSuccessHandler(log).getActiveRange();
},200)
})
log(e) => {
$('#data').val(e)
}
Question.
If someone did something similar, please share your experience.
Or tell me why this example does not work. If he can be reanimated, I will adapt him to solve my task.
I worked on a similar project and here's the solution:
function onSelectionChange(e)
{
var ss = e.source;
var Sh = ss.getActiveSheet();
var range = Sh.getRange("A1:A300");
var target = e.source.getActiveRange();
//check for intersection
if(RangeIntersects(target, range))
{
Logger.log("Changed Row: " + target.getRow() + "\nValue: " + target.getValue());
}
}
//returns true if target intersects with the predefined range
function RangeIntersects(target, range)
{
return (target.getLastRow() >= range.getRow()) && (range.getLastRow() >= target.getRow()) && (target.getLastColumn() >= range.getColumn()) && (range.getLastColumn() >= target.getColumn());
}
Here's an idea. I can't quite get it to work though.
Maybe someone else can give a better answer.
Also, having functions running 24/7 is not possible with GAS, I think, as there are limits to the total run-time. You may wish to add a code-guard that exits the script if the last update time is longer than 10 minutes ago or something.
function checkSelection() {
var spreadsheet = SpreadsheetApp.getActive();
var targetRange = spreadsheet.getRange('activate');
// Change your named ranged name here
var tCol = targetRange.getColumn();
var tLastCol = targetRange.getLastColumn();
var tRow = targetRange.getRow();
var tLastRow = targetRange.getLastRow();
var num = 0;
for (num; num < 115; ++num) {
// Repeats the code below 100 times
var range = spreadsheet.getActiveRange();
var row = range.getRow();
var col = range.getColumn();
if (col >= tCol && col <= tLastCol && row >= tRow && row <= tLastRow) {
range.setBackground('#000000');
// Change the code in this block to your code.
}
SpreadsheetApp.flush();
Utilities.sleep(500);
// Waits half a second before repeating
}
}
115 repetitions * 500ms wait seems to run for almost a minute, then the trigger will fire the whole function again.
Intersection of two Ranges
You can use this to calculate intersection of two ranges. It requires an object in the form of: {rg1:'A1Notation String',rg2:'A1Notation String'}
function calculateIntersection1(rgObj) {
var iObj={};
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg1=sh.getRange(rgObj.rg1);
var rg2=sh.getRange(rgObj.rg2);
var iObj={rg1colst:rg1.getColumn(),rg1colen:rg1.getColumn()+rg1.getWidth()-1,rg1rowst:rg1.getRow(),rg1rowen:rg1.getRow()+rg1.getHeight()-1,rg2colst:rg2.getColumn(),rg2colen:rg2.getColumn()+rg2.getWidth()-1,rg2rowst:rg2.getRow(),rg2rowen:rg2.getRow()+rg2.getHeight()-1};
if(iObj.rg1colst>iObj.rg2colen || iObj.rg1colen<iObj.rg2colst || iObj.rg1rowst>iObj.rg2rowen || iObj.rg1rowen<iObj.rg2rowst || iObj.rg2colst>iObj.rg1colen || iObj.rg2colen<iObj.rg1colst || iObj.rg2rowst>iObj.rg1rowen || iObj.rg2rowen<iObj.rg1rowst) {
return '<h1>No intersecting cells</h1>';
}else{
var vA1=rg1.getValues();
var v1=[];
var vA2=rg2.getValues();
var v2=[];
for(var i=0;i<vA1.length;i++){
for(var j=0;j<vA1[i].length;j++){
var s=Utilities.formatString('(%s,%s)', iObj.rg1rowst+i,iObj.rg1colst+j);
v1.push(s);
}
}
for(var i=0;i<vA2.length;i++){
for(var j=0;j<vA2[i].length;j++){
var s=Utilities.formatString('(%s,%s)', iObj.rg2rowst+i,iObj.rg2colst+j);
v2.push(s);
}
}
var oA=[];
for(var i=0;i<v1.length;i++){
var idx=v2.indexOf(v1[i]);
if(idx>-1){
oA.push(v2[idx]);
}
}
return Utilities.formatString('Intersecting Cells: %s', oA.join(', '));
}
}
It either returns the string "No Intersecting Cells" or a string identifying the intersecting cells in (row, column) format.

Excel Javascript (Office.js) - LastRow/LastColumn - better alternative?

I have been a fervent reader of StackOverflow over the last few years, and I was able to resolve pretty much everything in VBA Excel with a search and some adapting. I never felt the need to post any questions before, so I do apologize if this somehow duplicates something else, or there is an answer to this already and I couldn't find it.
Now I`m considering Excel-JS in order to create an AddIn (or more), but have to say that Javascript is not exactly my bread and butter. Over the time of using VBA, I find that one of the most simple and common needs is to get the last row in a sheet or given range, and maybe less often the last column.
I've managed to put some code together in Javascript to get similar functionality, and as it is... it works. There are 2 reasons I`m posting this
Looking to improve the code, and my knowledge
Maybe someone else can make use of the code meanwhile
So... in order to get my lastrow/lastcolumn, I use global variables:
var globalLastRow = 0; //Get the last row in used range
var globalLastCol = 0; //Get the last column in used range
Populate the global variables with the function to return lastrow/lastcolumn:
function lastRC(wsName) {
return Excel.run(function (context) {
var wsTarget = context.workbook.worksheets.getItem(wsName);
//Get last row/column from used range
var uRange = wsTarget.getUsedRange();
uRange.load(['rowCount', 'columnCount']);
return context.sync()
.then(function () {
globalLastRow = uRange.rowCount;
globalLastCol = uRange.columnCount;
});
});
}
And lastly get the value where I need them in other functions:
var lRow = 0; var lCol = 0;
await lastRC("randomSheetName");
lRow = globalLastRow; lCol = globalLastCol;
I`m mainly interested if I can return the values directly from the function lastRC (and how...), rather than go around with this solution.
Any suggestions are greatly appreciated (ideally if they don't come with stones attached).
EDIT:
I've gave up on using an extra function for this as for now, given that it uses extra context.sync, and as I've read since this post, the less syncs, the better.
Also, the method above is only good, as long your usedrange starts in cell "A1" (or well, in the first row/column at least), otherwise a row/column count is not exactly helpful, when you need the index.
Luckily, there is another method to get the last row/column:
var uRowsIndex = ws.getCell(0, 0).getEntireColumn().getUsedRange().getLastCell().load(['rowIndex']);
var uColsIndex = ws.getCell(0, 0).getEntireRow().getUsedRange().getLastCell().load(['columnIndex']);
To break down one of this examples, you are:
starting at cell "A1" getCell(0, 0)
select the entire column "A:A" getEntireColumn()
select the usedrange in that column getUsedRange() (i.e.: "A1:A12")
select the last cell in the used range getLastCell() (i.e.: "A12")
load the row index load(['rowIndex']) (for "A12" rowIndex = 11)
If your data is constant, and you don't need to check lastrow at specific column (or last column at specific row), then the shorter version of the above is:
uIndex = ws.getUsedRange().getLastCell().load(['rowIndex', 'columnIndex']);
Lastly, keep in mind that usedrange will consider formatting as well, not just values, so if you have formatted rows under your data, expect the unexpected.
late edit - you can specify if you want your used range to be of values only (thanks Ethan):
getUsedRange(valuesOnly?: boolean): Excel.Range;
I have to say a big thank you to Michael Zlatkovsky who has put a lot of work, in a lot of documentation, which I`m far from finishing to read.

Google Doc referencing another sheet

After speaking to Google Enterprise Support they suggested I create a post on Stackoverflow.
I have a Google Doc sheet with a list of stores (sheet A) and I'm trying to reference another sheet (sheet B) to VOID specific stores.
What I'm going to accomplish is if a store name on the void sheet is entered into sheet A it will automatically convert the store name to VOID.
Google support believes an IF statement would be the beginning to the solution, they weren't able to help beyond this.
For anyone's time that comes up with a solution, I'd be happy to buy you a couple Starbucks coffees. Your support means a lot.
make it simple using Google Scripts. (Tutorial)
To edit scripts do: Tools -> Script Editor
and in the current add this function
EDIT:
Well, you need to make a trigger. In this case will be when the current sheet is edited
Here is the javascript
function onEdit(event) {
// get actual spreadsheet
var actual = event.source.getActiveSheet();
// Returns the active cell
var cellSheet1 = actual.getActiveCell();
// get actual range
var range = event.source.getActiveRange();
// get active spreadsheet
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// get sheets
var sheet_two = activeSpreadsheet.getSheetByName("Sheet2");
range.copyValuesToRange(sheet_two, range.getColumn(), range.getColumn(), range.getRow(), range.getRow());
// get cell from sheet2
var cell = sheet_two.getRange(range.getValue());
cell.setValue(cellSheet1.getValue());
// display log
Logger.log(cellSheet1.getValue());
}
if you want to test it you can check my spreadsheet, there you can check that all data thats is inserted in sheet1 will be copied to sheet2

Resources