I decided to break this onEdit(e) trigger function up into multiple functions, but when I did, the event objects (e) portion got "lost." After messing with it for a while, I finally got it to work again, but I don't think it's the most efficient solution.
Any suggestions, or is this good enough? Basically, I just added var e = e; and that made it work again.
function onEdit(e){
Logger.log(e);
if(e.range.getSheet().getName() == 'Estimate'){
var e = e;
Logger.log("Starting subCatDV...");
subCatDV(e);
Logger.log("Finished subCatDV!");
Logger.log("Starting itemDV...");
itemDV(e);
Logger.log("Finished itemDV!");
Logger.log("Starting subItemDV...");
subItemDV(e);
Logger.log("Finished subItemDV!");
}
if(e.range.getSheet().getName() == 'Items'){
subCatDV();
}
return;
}
Here is the function that didn't seem to be receiving the event objects
function subItemDV(e){
// Populate sub-item data validations
var estss = SpreadsheetApp.getActive().getSheetByName('Estimate');
var itemss = SpreadsheetApp.getActive().getSheetByName('Items');
var subItemDVss = SpreadsheetApp.getActive().getSheetByName('subItemDataValidations');
var activeCell = estss.getActiveCell();
Logger.log("I'm in subItemDV...");
Logger.log(e);
Logger.log(activeCell);
Logger.log("Checking sheet name...");
if(activeCell.getColumn() == 3 && activeCell.getRow() > 1){
if(e.range.getSheet().getName() == 'Items') return;
Logger.log("Not in 'Items' sheet! Moving on...");
activeCell.offset(0, 1).clearContent().clearDataValidations();
var subItem = subItemDVss.getRange(activeCell.getRow(),activeCell.getColumn(),itemss.getLastColumn()).getValues();
var subItemIndex = subItem[0].indexOf(activeCell.getValue()) + 2;
Logger.log("Checking subItemIndex...");
if(subItemIndex != 0){
var subItemValidationRange = subItemDVss.getRange(activeCell.getRow(),4,1,subItemDVss.getLastColumn());
var subItemValidationRule = SpreadsheetApp.newDataValidation().requireValueInRange(subItemValidationRange).build();
activeCell.offset(0, 1).setDataValidation(subItemValidationRule);
Logger.log("Finished checking subItemIndex...");
}
}
}
So as not to inflate discussion in comments: you can safely remove the var e = e assignment from the script, as it does not affect the problems that your updated version of the script solved:
e is an event object that is constructed as a response to trigger being fired. Since in your case the trigger is an onEdit(e) trigger, event object is undefined until an edit is made in the target Spreadsheet (please, note that script-triggered edits do not count);
Even if you called the function with a parameter (like doSomething(e)), in case you either do not access the parameter via the arguments object, or explicitly define it in a function declaration function doSomething(e), the event object won't be persisted;
Also, you might've missed the e binding in the last subCatDV() call + the if statement can be optimized (btw, don't use the equality comparison, use the identity comparison instead, it will save you debugging time in the future):
var name = e.range.getSheet().getName();
if(name === 'Estimate') {
doSomething(e);
}else if(name === 'Items') { //identity comparison ensures type match;
doSomethingElse(e);
}
Useful links
event object reference;
arguments object reference;
Related
I don't uderstand why my function doesn't recognize the 'e' from the onEdit function?
Do you have an idea?
Thank's a lot if you can help me
the error when i try to debug
function onEdit(e){
var feuille = e.source;
if(SpreadsheetApp.getActiveRange().getColumn()==7){ //the 7th column is the "+/-" column
var i = e.source.getActiveRange().getRow(); //i is the line of the change
var k = 408+i; // k is the line where we put the date
var l = 2; //l is the column where we put the date and the quantity
var cellDate = feuille.getRange(k,l);
while (isEmpty(cellDate)==false){ //the column of the date is incremented
l=l+1;
}
feuille.getRange(k,l).setValue(new Date());
const stockDate = feuille.getRange(i,8).getValue();
feuille.getRange(k+1,8).setValue(stockDate);
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange("G2:G403").setValue('0'); ////we reset the "+/-" column to zero
}
}
The onEdit(e) function is a simple trigger that is designed to run automatically when you hand edit the spreadsheet. In that context, the event object e is properly populated.
If you blindly run your onEdit(e) function in the script editor, the event parameter e is not populated, causing the error you mention.
You can debug your onEdit(e) function by writing a wrapper function that creates an object and uses it as parameter when calling onEdit(e), like this:
function testOnEdit() {
const e = {
source: SpreadsheetApp.getActive(),
};
onEdit(e);
}
Then debug the testOnEdit() function.
See event object.
I am familiar with the Google Apps script DataValidation object. To get and set validation criteria. But how to tell programatically if a cell value is actually valid. So I can see the little red validation fail message in the spreadsheet but can the fact the cell is currently failing validation be picked up thru code?
I have tried to see if there is a cell property that tells you this but there is not. Also I looked for some sort of DataValidation "validate" method - i.e. test a value against validation rules, but nothing there either
Any ideas? Is this possible??
Specific answer to your question, there is no method within Google Apps Script that will return the validity of a Range such as .isValid(). As you state, you could reverse engineer a programatic one using Range.getDataValidations() and then parsing the results of that in order to validate again the values of a Range.getValues() call.
It's a good suggestion. I've added a feature request to the issue tracker -> Add a Star to vote it up.
I've created a workaround for this issue that works in a very ugly -technically said- and slightly undetermined way.
About the workaround:
It works based on the experience that the web browser implementation of catch() function allows to access thrown errors from the Google's JS code parts.
In case an invalid input into a cell is rejected by a validation rule then the system will display an error message that is catchable by the user written GAS. In order to make it work first the reject value has to be set on the specified cell then its vale has to be re-entered (modified) then -right after this- calling the getDataValidation() built in function allows the user to catch the necessary error.
Only single cells can be tested with this method as setCellValues() ignores any data validation restriction (as of today).
Disadvantages:
The validity won't be necessarily re-checked for this function:
it calls a cell validation function right after the value is inserted into the cell.
Therefore the result of this function might be faulty.
The code messes up the history as cells will be changed - in case they are
valid.
I've tested it successfully on both Firefox and Chromium.
function getCellValidity(cell) {
var origValidRule = cell.getDataValidation();
if (origValidRule == null || ! (cell.getNumRows() == cell.getNumColumns() == 1)) {
return null;
}
var cell_value = cell.getValue();
if (cell_value === '') return true; // empty cell is always valid
var is_valid = true;
var cell_formula = cell.getFormula();
// Storing and checking if cell validation is set to allow invalid input with a warning or reject it
var reject_invalid = ! origValidRule.getAllowInvalid();
// If invalid value is allowed (just warning), then changing validation to reject it
// IMPORTANT: this will not throw an error!
if (! reject_invalid) {
var rejectValidRule = origValidRule.copy().setAllowInvalid(false).build();
cell.setDataValidation(rejectValidRule);
}
// Re-entering value or formula into the cell itself
var cell_formula = cell.getFormula();
if (cell_formula !== '') {
cell.setFormula(cell_formula);
} else {
cell.setValue(cell_value);
}
try {
var tempValidRule = cell.getDataValidation();
} catch(e) {
// Exception: The data that you entered in cell XY violates the data validation rules set on this cell.
// where XY is the A1 style address of the cell
is_valid = false;
}
// Restoring original rule
if (rejectValidRule != null) {
cell.setDataValidation(origValidRule.copy().setAllowInvalid(true).build());
}
return is_valid;
}
I still recommend starring the above Google bug report opened by Jonathon.
I'm using this solution. Simple to learn and fast to use! You may need to adapt this code for your needs. Hope you enjoy
function test_corr(link,name) {
var ss = SpreadsheetApp.openByUrl(link).getSheetByName(name);
var values = ss.getRange(2,3,200,1).getValues();
var types = ss.getRange(2,3,200,1).getDataValidations()
var ans
for (var i = 0; i < types.length; i++) {
if (types[i][0] != null){
var type = types[i][0].getCriteriaType()
var dval_values = types[i][0].getCriteriaValues()
ans = false
if (type == "VALUE_IN_LIST") {
for (var j = 0; j < dval_values[0].length; j++) {
if (dval_values[0][j] == values[i][0]) { ans = true }
}
} else if (type == "NUMBER_BETWEEN") {
if (values[i][0] >= dval_values[0] && values[i][0] <= dval_values[1]) { ans = true }
} else if (type == "CHECKBOX") {
if (values[i][0] == "Да" || values[i][0] == "Нет") { ans = true }
}
if (!ans) { return false }
}
}
return true;
}
The question is: how to check if the d3 behavior, such as drag or zoom, is already called on the element? Seems to be this task is in relation to getting the events, which are attached to element? What is the common approach?
Thanks.
I know this is old, but I came across this post looking for the same thing, saw there wasn't an answer, so created the answer my self. This is for d3.js Version 4.
function hasD3EventOn(ele, eventName, eventType) {
if (!ele || typeof eventName == 'undefined') return !1;
if (ele._groups) ele = ele._groups[0][0]; // if d3.select()
var on = ele.__on; //d3.js(v4) function onRemove(typename)
if (!on || !on.length) return !1; var i = on.length, o;
while (--i >= 0) { if ((o = on[i]).name && o.name === eventName && (typeof eventType == 'undefined' || (o.type && o.type === eventType))) return !0; } return !1;
}
Just send in either a d3.select element (first in array only) or a native DOM Element along with the event name such as 'zoom', both are reqired. There is also the option for the event type such as 'wheel'. It also has some basic validation (maybe excessive) to help prevent crashing when called with trash or property name collison.
I have written a multiselect jQuery plugin that can be applied to a normal HTML select element.
However, this plugin will parse the select element and its options and then remove the select element from the DOM and insert a combination of divs and checkboxes instead.
I have created a custom binding handler in Knockout as follows:
ko.bindingHandlers.dropdownlist = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// This will be called when the binding is first applied to an element
// Set up any initial state, event handlers, etc. here
// Retrieve the value accessor
var value = valueAccessor();
// Get the true value of the property
var unwrappedValue = ko.utils.unwrapObservable(value);
// Check if we have specified the value type of the DropDownList items. Defaults to "int"
var ddlValueType = allBindingsAccessor().dropDownListValueType ? allBindingsAccessor().dropDownListValueType : 'int';
// Check if we have specified the INIMultiSelect options otherwise we will use our defaults.
var elementOptions = allBindingsAccessor().iniMultiSelectOptions ? allBindingsAccessor().iniMultiSelectOptions :
{
multiple: false,
onItemSelectedChanged: function (control, item) {
var val = item.value;
if (ddlValueType === "int") {
value(parseInt(val));
}
else if (ddlValueType == "float") {
value(parseFloat(val));
} else {
value(val);
}
}
};
// Retrieve the attr: {} binding
var attribs = allBindingsAccessor().attr;
// Check if we specified the attr binding
if (attribs != null && attribs != undefined) {
// Check if we specified the attr ID binding
if (attribs.hasOwnProperty('id')) {
var id = attribs.id;
$(element).attr('id', id);
}
if (bindingContext.hasOwnProperty('$index')) {
var idx = bindingContext.$index();
$(element).attr('name', 'ddl' + idx);
}
}
if ($(element).attr('id') == undefined || $(element).attr('id') == '') {
var id = "ko_ddl_id_" + (ko.bindingHandlers['dropdownlist'].currentIndex);
$(element).attr('id', id);
}
if ($(element).attr('name') == undefined || $(element).attr('name') == '') {
var name = "ko_ddl_name_" + (ko.bindingHandlers['dropdownlist'].currentIndex);
$(element).attr('name', name);
}
var options = $('option', element);
$.each(options, function (index) {
if ($(this).val() == unwrappedValue) {
$(this).attr('selected', 'selected');
}
});
if (!$(element).hasClass('INIMultiSelect')) {
$(element).addClass('INIMultiSelect');
}
$(element).iniMultiSelect(elementOptions);
ko.bindingHandlers['dropdownlist'].currentIndex++;
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var unwrappedValue = ko.utils.unwrapObservable(valueAccessor());
var id = $(element).attr('id').replace(/\[/gm, '\\[').replace(/\]/gm, '\\]');
var iniMultiSelect = $('#' + id);
if (iniMultiSelect != null) {
iniMultiSelect.SetValue(unwrappedValue, true);
}
}};
ko.bindingHandlers.dropdownlist.currentIndex = 0;
This will transform the original HTML select element into my custom multiselect.
However, when the update function is called the first time, after the init, the "element" variable will still be the original select element, and not my wrapper div that holds my custom html together.
And after the page has been completely loaded and I change the value of the observable that I am binding to, the update function is not triggered at all!
Somehow I have a feeling that knockout no longer "knows" what to do because the original DOM element that I'm binding to is gone...
Any ideas what might be the issue here?
There is clean up code in Knockout that will dispose of the computed observables that are used to trigger bindings when it determines that the element is no longer part of the document.
You could potentially find a way to just hide the original element, or place the binding on a container of the original select (probably would be a good option), or reapply a binding to one of the new elements.
I ran into a similar problem today, and here's how I solved it. In my update handler, I added the following line:
$(element).attr("dummy-attribute", ko.unwrap(valueAccessor()));
This suffices to prevent the handler from being disposed-of by Knockout's garbage collector.
JSFiddle (broken): http://jsfiddle.net/padfv0u9/
JSFiddle (fixed): http://jsfiddle.net/padfv0u9/2/
I have decent large data set of around 1100 records. This data set is mapped to an observable array which is then bound to a view. Since these records are updated frequently, the observable array is updated every time using the ko.mapping.fromJS helper.
This particular command takes around 40s to process all the rows. The user interface just locks for that period of time.
Here is the code -
var transactionList = ko.mapping.fromJS([]);
//Getting the latest transactions which are around 1100 in number;
var data = storage.transactions();
//Mapping the data to the observable array, which takes around 40s
ko.mapping.fromJS(data,transactionList)
Is there a workaround for this? Or should I just opt of web workers to improve performances?
Knockout.viewmodel is a replacement for knockout.mapping that is significantly faster at creating viewmodels for large object arrays like this. You should notice a significant performance increase.
http://coderenaissance.github.com/knockout.viewmodel/
I have also thought of a workaround as follows, this uses less amount of code-
var transactionList = ko.mapping.fromJS([]);
//Getting the latest transactions which are around 1100 in number;
var data = storage.transactions();
//Mapping the data to the observable array, which takes around 40s
// Instead of - ko.mapping.fromJS(data,transactionList)
var i = 0;
//clear the list completely first
transactionList.destroyAll();
//Set an interval of 0 and keep pushing the content to the list one by one.
var interval = setInterval(function () {if (i == data.length - 1 ) {
clearInterval(interval);}
transactionList.push(ko.mapping.fromJS(data[i++]));
}, 0);
I had the same problem with mapping plugin. Knockout team says that mapping plugin is not intended to work with large arrays. If you have to load such big data to the page then likely you have improper design of the system.
The best way to fix this is to use server pagination instead of loading all the data on page load. If you don't want to change design of your application there are some workarounds which maybe help you:
Map your array manually:
var data = storage.transactions();
var mappedData = ko.utils.arrayMap(data , function(item){
return ko.mapping.fromJS(item);
});
var transactionList = ko.observableArray(mappedData);
Map array asynchronously. I have written a function that processes array by portions in another thread and reports progress to the user:
function processArrayAsync(array, itemFunc, afterStepFunc, finishFunc) {
var itemsPerStep = 20;
var processor = new function () {
var self = this;
self.array = array;
self.processedCount = 0;
self.itemFunc = itemFunc;
self.afterStepFunc = afterStepFunc;
self.finishFunc = finishFunc;
self.step = function () {
var tillCount = Math.min(self.processedCount + itemsPerStep, self.array.length);
for (; self.processedCount < tillCount; self.processedCount++) {
self.itemFunc(self.array[self.processedCount], self.processedCount);
}
self.afterStepFunc(self.processedCount);
if (self.processedCount < self.array.length - 1)
setTimeout(self.step, 1);
else
self.finishFunc();
};
};
processor.step();
};
Your code:
var data = storage.transactions();
var transactionList = ko.observableArray([]);
processArrayAsync(data,
function (item) { // Step function
var transaction = ko.mapping.fromJS(item);
transactionList().push(transaction);
},
function (processedCount) {
var percent = Math.ceil(processedCount * 100 / data.length);
// Show progress to the user.
ShowMessage(percent);
},
function () { // Final function
// This function will fire when all data are mapped. Do some work (i.e. Apply bindings).
});
Also you can try alternative mapping library: knockout.wrap. It should be faster than mapping plugin.
I have chosen the second option.
Mapping is not magic. In most of the cases this simple recursive function can be sufficient:
function MyMapJS(a_what, a_path)
{
a_path = a_path || [];
if (a_what != null && a_what.constructor == Object)
{
var result = {};
for (var key in a_what)
result[key] = MyMapJS(a_what[key], a_path.concat(key));
return result;
}
if (a_what != null && a_what.constructor == Array)
{
var result = ko.observableArray();
for (var index in a_what)
result.push(MyMapJS(a_what[index], a_path.concat(index)));
return result;
}
// Write your condition here:
switch (a_path[a_path.length-1])
{
case 'mapThisProperty':
case 'andAlsoThisOne':
result = ko.observable(a_what);
break;
default:
result = a_what;
break;
}
return result;
}
The code above makes observables from the mapThisProperty and andAlsoThisOne properties at any level of the object hierarchy; other properties are left constant. You can express more complex conditions using a_path.length for the level (depth) the value is at, or using more elements of a_path. For example:
if (a_path.length >= 2
&& a_path[a_path.length-1] == 'mapThisProperty'
&& a_path[a_path.length-2] == 'insideThisProperty')
result = ko.observable(a_what);
You can use typeOf a_what in the condition, e.g. to make all strings observable.
You can ignore some properties, and insert new ones at certain levels.
Or, you can even omit a_path. Etc.
The advantages are:
Customizable (more easily than knockout.mapping).
Short enough to copy-paste it and write individual mappings for different objects if needed.
Smaller code, knockout.mapping-latest.js is not included into your page.
Should be faster as it does only what is absolutely necessary.