/**
* Final initialisation of the builder
* @param {object} [rules]
* @fires QueryBuilder.afterInit
* @private
*/
QueryBuilder.prototype.init = function(rules) {
/**
* When the initilization is done, just before creating the root group
* @event afterInit
* @memberof QueryBuilder
*/
this.trigger('afterInit');
if (rules) {
this.setRules(rules);
delete this.settings.rules;
}
else {
this.setRoot(true);
}
};
/**
* Checks the configuration of each filter
* @param {QueryBuilder.Filter[]} filters
* @returns {QueryBuilder.Filter[]}
* @throws ConfigError
*/
QueryBuilder.prototype.checkFilters = function(filters) {
var definedFilters = [];
if (!filters || filters.length === 0) {
Utils.error('Config', 'Missing filters list');
}
filters.forEach(function(filter, i) {
if (!filter.id) {
Utils.error('Config', 'Missing filter {0} id', i);
}
if (definedFilters.indexOf(filter.id) != -1) {
Utils.error('Config', 'Filter "{0}" already defined', filter.id);
}
definedFilters.push(filter.id);
if (!filter.type) {
filter.type = 'string';
}
else if (!QueryBuilder.types[filter.type]) {
Utils.error('Config', 'Invalid type "{0}"', filter.type);
}
if (!filter.input) {
filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
}
else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
Utils.error('Config', 'Invalid input "{0}"', filter.input);
}
if (filter.operators) {
filter.operators.forEach(function(operator) {
if (typeof operator != 'string') {
Utils.error('Config', 'Filter operators must be global operators types (string)');
}
});
}
if (!filter.field) {
filter.field = filter.id;
}
if (!filter.label) {
filter.label = filter.field;
}
if (!filter.optgroup) {
filter.optgroup = null;
}
else {
this.status.has_optgroup = true;
// register optgroup if needed
if (!this.settings.optgroups[filter.optgroup]) {
this.settings.optgroups[filter.optgroup] = filter.optgroup;
}
}
switch (filter.input) {
case 'radio':
case 'checkbox':
if (!filter.values || filter.values.length < 1) {
Utils.error('Config', 'Missing filter "{0}" values', filter.id);
}
break;
case 'select':
var cleanValues = [];
filter.has_optgroup = false;
Utils.iterateOptions(filter.values, function(value, label, optgroup) {
cleanValues.push({
value: value,
label: label,
optgroup: optgroup || null
});
if (optgroup) {
filter.has_optgroup = true;
// register optgroup if needed
if (!this.settings.optgroups[optgroup]) {
this.settings.optgroups[optgroup] = optgroup;
}
}
}.bind(this));
if (filter.has_optgroup) {
filter.values = Utils.groupSort(cleanValues, 'optgroup');
}
else {
filter.values = cleanValues;
}
if (filter.placeholder) {
if (filter.placeholder_value === undefined) {
filter.placeholder_value = -1;
}
filter.values.forEach(function(entry) {
if (entry.value == filter.placeholder_value) {
Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
}
});
}
break;
}
}, this);
if (this.settings.sort_filters) {
if (typeof this.settings.sort_filters == 'function') {
filters.sort(this.settings.sort_filters);
}
else {
var self = this;
filters.sort(function(a, b) {
return self.translate(a.label).localeCompare(self.translate(b.label));
});
}
}
if (this.status.has_optgroup) {
filters = Utils.groupSort(filters, 'optgroup');
}
return filters;
};
/**
* Checks the configuration of each operator
* @param {QueryBuilder.Operator[]} operators
* @returns {QueryBuilder.Operator[]}
* @throws ConfigError
*/
QueryBuilder.prototype.checkOperators = function(operators) {
var definedOperators = [];
operators.forEach(function(operator, i) {
if (typeof operator == 'string') {
if (!QueryBuilder.OPERATORS[operator]) {
Utils.error('Config', 'Unknown operator "{0}"', operator);
}
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
}
else {
if (!operator.type) {
Utils.error('Config', 'Missing "type" for operator {0}', i);
}
if (QueryBuilder.OPERATORS[operator.type]) {
operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
}
if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
}
}
if (definedOperators.indexOf(operator.type) != -1) {
Utils.error('Config', 'Operator "{0}" already defined', operator.type);
}
definedOperators.push(operator.type);
if (!operator.optgroup) {
operator.optgroup = null;
}
else {
this.status.has_operator_optgroup = true;
// register optgroup if needed
if (!this.settings.optgroups[operator.optgroup]) {
this.settings.optgroups[operator.optgroup] = operator.optgroup;
}
}
}, this);
if (this.status.has_operator_optgroup) {
operators = Utils.groupSort(operators, 'optgroup');
}
return operators;
};
/**
* Adds all events listeners to the builder
* @private
*/
QueryBuilder.prototype.bindEvents = function() {
var self = this;
var Selectors = QueryBuilder.selectors;
// group condition change
this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
if ($(this).is(':checked')) {
var $group = $(this).closest(Selectors.group_container);
self.getModel($group).condition = $(this).val();
}
});
// rule filter change
this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
var $rule = $(this).closest(Selectors.rule_container);
self.getModel($rule).filter = self.getFilterById($(this).val());
});
// rule operator change
this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
var $rule = $(this).closest(Selectors.rule_container);
self.getModel($rule).operator = self.getOperatorByType($(this).val());
});
// add rule button
this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
var $group = $(this).closest(Selectors.group_container);
self.addRule(self.getModel($group));
});
// delete rule button
this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
var $rule = $(this).closest(Selectors.rule_container);
self.deleteRule(self.getModel($rule));
});
if (this.settings.allow_groups !== 0) {
// add group button
this.$el.on('click.queryBuilder', Selectors.add_group, function() {
var $group = $(this).closest(Selectors.group_container);
self.addGroup(self.getModel($group));
});
// delete group button
this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
var $group = $(this).closest(Selectors.group_container);
self.deleteGroup(self.getModel($group));
});
}
// model events
this.model.on({
'drop': function(e, node) {
node.$el.remove();
self.refreshGroupsConditions();
},
'add': function(e, parent, node, index) {
if (index === 0) {
node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
}
else {
node.$el.insertAfter(parent.rules[index - 1].$el);
}
self.refreshGroupsConditions();
},
'move': function(e, node, group, index) {
node.$el.detach();
if (index === 0) {
node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
}
else {
node.$el.insertAfter(group.rules[index - 1].$el);
}
self.refreshGroupsConditions();
},
'update': function(e, node, field, value, oldValue) {
if (node instanceof Rule) {
switch (field) {
case 'error':
self.updateError(node);
break;
case 'flags':
self.applyRuleFlags(node);
break;
case 'filter':
self.updateRuleFilter(node, oldValue);
break;
case 'operator':
self.updateRuleOperator(node, oldValue);
break;
case 'value':
self.updateRuleValue(node, oldValue);
break;
}
}
else {
switch (field) {
case 'error':
self.updateError(node);
break;
case 'flags':
self.applyGroupFlags(node);
break;
case 'condition':
self.updateGroupCondition(node, oldValue);
break;
}
}
}
});
};
/**
* Creates the root group
* @param {boolean} [addRule=true] - adds a default empty rule
* @param {object} [data] - group custom data
* @param {object} [flags] - flags to apply to the group
* @returns {Group} root group
* @fires QueryBuilder.afterAddGroup
*/
QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
addRule = (addRule === undefined || addRule === true);
var group_id = this.nextGroupId();
var $group = $(this.getGroupTemplate(group_id, 1));
this.$el.append($group);
this.model.root = new Group(null, $group);
this.model.root.model = this.model;
this.model.root.data = data;
this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
this.model.root.condition = this.settings.default_condition;
this.trigger('afterAddGroup', this.model.root);
if (addRule) {
this.addRule(this.model.root);
}
return this.model.root;
};
/**
* Adds a new group
* @param {Group} parent
* @param {boolean} [addRule=true] - adds a default empty rule
* @param {object} [data] - group custom data
* @param {object} [flags] - flags to apply to the group
* @returns {Group}
* @fires QueryBuilder.beforeAddGroup
* @fires QueryBuilder.afterAddGroup
*/
QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
addRule = (addRule === undefined || addRule === true);
var level = parent.level + 1;
/**
* Just before adding a group, can be prevented.
* @event beforeAddGroup
* @memberof QueryBuilder
* @param {Group} parent
* @param {boolean} addRule - if an empty rule will be added in the group
* @param {int} level - nesting level of the group, 1 is the root group
*/
var e = this.trigger('beforeAddGroup', parent, addRule, level);
if (e.isDefaultPrevented()) {
return null;
}
var group_id = this.nextGroupId();
var $group = $(this.getGroupTemplate(group_id, level));
var model = parent.addGroup($group);
model.data = data;
model.flags = $.extend({}, this.settings.default_group_flags, flags);
model.condition = this.settings.default_condition;
/**
* Just after adding a group
* @event afterAddGroup
* @memberof QueryBuilder
* @param {Group} group
*/
this.trigger('afterAddGroup', model);
/**
* After any change in the rules
* @event rulesChanged
* @memberof QueryBuilder
*/
this.trigger('rulesChanged');
if (addRule) {
this.addRule(model);
}
return model;
};
/**
* Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
* @param {Group} group
* @returns {boolean} if the group has been deleted
* @fires QueryBuilder.beforeDeleteGroup
* @fires QueryBuilder.afterDeleteGroup
*/
QueryBuilder.prototype.deleteGroup = function(group) {
if (group.isRoot()) {
return false;
}
/**
* Just before deleting a group, can be prevented
* @event beforeDeleteGroup
* @memberof QueryBuilder
* @param {Group} parent
*/
var e = this.trigger('beforeDeleteGroup', group);
if (e.isDefaultPrevented()) {
return false;
}
var del = true;
group.each('reverse', function(rule) {
del &= this.deleteRule(rule);
}, function(group) {
del &= this.deleteGroup(group);
}, this);
if (del) {
group.drop();
/**
* Just after deleting a group
* @event afterDeleteGroup
* @memberof QueryBuilder
*/
this.trigger('afterDeleteGroup');
this.trigger('rulesChanged');
}
return del;
};
/**
* Performs actions when a group's condition changes
* @param {Group} group
* @param {object} previousCondition
* @fires QueryBuilder.afterUpdateGroupCondition
* @private
*/
QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
var $this = $(this);
$this.prop('checked', $this.val() === group.condition);
$this.parent().toggleClass('active', $this.val() === group.condition);
});
/**
* After the group condition has been modified
* @event afterUpdateGroupCondition
* @memberof QueryBuilder
* @param {Group} group
* @param {object} previousCondition
*/
this.trigger('afterUpdateGroupCondition', group, previousCondition);
this.trigger('rulesChanged');
};
/**
* Updates the visibility of conditions based on number of rules inside each group
* @private
*/
QueryBuilder.prototype.refreshGroupsConditions = function() {
(function walk(group) {
if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
.parent().toggleClass('disabled', group.rules.length <= 1);
}
group.each(null, function(group) {
walk(group);
}, this);
}(this.model.root));
};
/**
* Adds a new rule
* @param {Group} parent
* @param {object} [data] - rule custom data
* @param {object} [flags] - flags to apply to the rule
* @returns {Rule}
* @fires QueryBuilder.beforeAddRule
* @fires QueryBuilder.afterAddRule
* @fires QueryBuilder.changer:getDefaultFilter
*/
QueryBuilder.prototype.addRule = function(parent, data, flags) {
/**
* Just before adding a rule, can be prevented
* @event beforeAddRule
* @memberof QueryBuilder
* @param {Group} parent
*/
var e = this.trigger('beforeAddRule', parent);
if (e.isDefaultPrevented()) {
return null;
}
var rule_id = this.nextRuleId();
var $rule = $(this.getRuleTemplate(rule_id));
var model = parent.addRule($rule);
model.data = data;
model.flags = $.extend({}, this.settings.default_rule_flags, flags);
/**
* Just after adding a rule
* @event afterAddRule
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterAddRule', model);
this.trigger('rulesChanged');
this.createRuleFilters(model);
if (this.settings.default_filter || !this.settings.display_empty_filter) {
/**
* Modifies the default filter for a rule
* @event changer:getDefaultFilter
* @memberof QueryBuilder
* @param {QueryBuilder.Filter} filter
* @param {Rule} rule
* @returns {QueryBuilder.Filter}
*/
model.filter = this.change('getDefaultFilter',
this.getFilterById(this.settings.default_filter || this.filters[0].id),
model
);
}
return model;
};
/**
* Tries to delete a rule
* @param {Rule} rule
* @returns {boolean} if the rule has been deleted
* @fires QueryBuilder.beforeDeleteRule
* @fires QueryBuilder.afterDeleteRule
*/
QueryBuilder.prototype.deleteRule = function(rule) {
if (rule.flags.no_delete) {
return false;
}
/**
* Just before deleting a rule, can be prevented
* @event beforeDeleteRule
* @memberof QueryBuilder
* @param {Rule} rule
*/
var e = this.trigger('beforeDeleteRule', rule);
if (e.isDefaultPrevented()) {
return false;
}
rule.drop();
/**
* Just after deleting a rule
* @event afterDeleteRule
* @memberof QueryBuilder
*/
this.trigger('afterDeleteRule');
this.trigger('rulesChanged');
return true;
};
/**
* Creates the filters for a rule
* @param {Rule} rule
* @fires QueryBuilder.changer:getRuleFilters
* @fires QueryBuilder.afterCreateRuleFilters
* @private
*/
QueryBuilder.prototype.createRuleFilters = function(rule) {
/**
* Modifies the list a filters available for a rule
* @event changer:getRuleFilters
* @memberof QueryBuilder
* @param {QueryBuilder.Filter[]} filters
* @param {Rule} rule
* @returns {QueryBuilder.Filter[]}
*/
var filters = this.change('getRuleFilters', this.filters, rule);
var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
/**
* After creating the dropdown for filters
* @event afterCreateRuleFilters
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterCreateRuleFilters', rule);
this.applyRuleFlags(rule);
};
/**
* Creates the operators for a rule and init the rule operator
* @param {Rule} rule
* @fires QueryBuilder.afterCreateRuleOperators
* @private
*/
QueryBuilder.prototype.createRuleOperators = function(rule) {
var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
if (!rule.filter) {
return;
}
var operators = this.getOperators(rule.filter);
var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
$operatorContainer.html($operatorSelect);
// set the operator without triggering update event
if (rule.filter.default_operator) {
rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
}
else {
rule.__.operator = operators[0];
}
rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
/**
* After creating the dropdown for operators
* @event afterCreateRuleOperators
* @memberof QueryBuilder
* @param {Rule} rule
* @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
*/
this.trigger('afterCreateRuleOperators', rule, operators);
this.applyRuleFlags(rule);
};
/**
* Creates the main input for a rule
* @param {Rule} rule
* @fires QueryBuilder.afterCreateRuleInput
* @private
*/
QueryBuilder.prototype.createRuleInput = function(rule) {
var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
rule.__.value = undefined;
if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
return;
}
var self = this;
var $inputs = $();
var filter = rule.filter;
for (var i = 0; i < rule.operator.nb_inputs; i++) {
var $ruleInput = $(this.getRuleInput(rule, i));
if (i > 0) $valueContainer.append(this.settings.inputs_separator);
$valueContainer.append($ruleInput);
$inputs = $inputs.add($ruleInput);
}
$valueContainer.css('display', '');
$inputs.on('change ' + (filter.input_event || ''), function() {
if (!rule._updating_input) {
rule._updating_value = true;
rule.value = self.getRuleInputValue(rule);
rule._updating_value = false;
}
});
if (filter.plugin) {
$inputs[filter.plugin](filter.plugin_config || {});
}
/**
* After creating the input for a rule and initializing optional plugin
* @event afterCreateRuleInput
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterCreateRuleInput', rule);
if (filter.default_value !== undefined) {
rule.value = filter.default_value;
}
else {
rule._updating_value = true;
rule.value = self.getRuleInputValue(rule);
rule._updating_value = false;
}
this.applyRuleFlags(rule);
};
/**
* Performs action when a rule's filter changes
* @param {Rule} rule
* @param {object} previousFilter
* @fires QueryBuilder.afterUpdateRuleFilter
* @private
*/
QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
this.createRuleOperators(rule);
this.createRuleInput(rule);
rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
// clear rule data if the filter changed
if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
rule.data = undefined;
}
/**
* After the filter has been updated and the operators and input re-created
* @event afterUpdateRuleFilter
* @memberof QueryBuilder
* @param {Rule} rule
* @param {object} previousFilter
*/
this.trigger('afterUpdateRuleFilter', rule, previousFilter);
this.trigger('rulesChanged');
};
/**
* Performs actions when a rule's operator changes
* @param {Rule} rule
* @param {object} previousOperator
* @fires QueryBuilder.afterUpdateRuleOperator
* @private
*/
QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
if (!rule.operator || rule.operator.nb_inputs === 0) {
$valueContainer.hide();
rule.__.value = undefined;
}
else {
$valueContainer.css('display', '');
if ($valueContainer.is(':empty') || !previousOperator ||
rule.operator.nb_inputs !== previousOperator.nb_inputs ||
rule.operator.optgroup !== previousOperator.optgroup
) {
this.createRuleInput(rule);
}
}
if (rule.operator) {
rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
// refresh value if the format changed for this operator
rule.__.value = this.getRuleInputValue(rule);
}
/**
* After the operator has been updated and the input optionally re-created
* @event afterUpdateRuleOperator
* @memberof QueryBuilder
* @param {Rule} rule
* @param {object} previousOperator
*/
this.trigger('afterUpdateRuleOperator', rule, previousOperator);
this.trigger('rulesChanged');
};
/**
* Performs actions when rule's value changes
* @param {Rule} rule
* @param {object} previousValue
* @fires QueryBuilder.afterUpdateRuleValue
* @private
*/
QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
if (!rule._updating_value) {
this.setRuleInputValue(rule, rule.value);
}
/**
* After the rule value has been modified
* @event afterUpdateRuleValue
* @memberof QueryBuilder
* @param {Rule} rule
* @param {*} previousValue
*/
this.trigger('afterUpdateRuleValue', rule, previousValue);
this.trigger('rulesChanged');
};
/**
* Changes a rule's properties depending on its flags
* @param {Rule} rule
* @fires QueryBuilder.afterApplyRuleFlags
* @private
*/
QueryBuilder.prototype.applyRuleFlags = function(rule) {
var flags = rule.flags;
var Selectors = QueryBuilder.selectors;
rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
if (flags.no_delete) {
rule.$el.find(Selectors.delete_rule).remove();
}
/**
* After rule's flags has been applied
* @event afterApplyRuleFlags
* @memberof QueryBuilder
* @param {Rule} rule
*/
this.trigger('afterApplyRuleFlags', rule);
};
/**
* Changes group's properties depending on its flags
* @param {Group} group
* @fires QueryBuilder.afterApplyGroupFlags
* @private
*/
QueryBuilder.prototype.applyGroupFlags = function(group) {
var flags = group.flags;
var Selectors = QueryBuilder.selectors;
group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
.parent().toggleClass('readonly', flags.condition_readonly);
if (flags.no_add_rule) {
group.$el.find(Selectors.add_rule).remove();
}
if (flags.no_add_group) {
group.$el.find(Selectors.add_group).remove();
}
if (flags.no_delete) {
group.$el.find(Selectors.delete_group).remove();
}
/**
* After group's flags has been applied
* @event afterApplyGroupFlags
* @memberof QueryBuilder
* @param {Group} group
*/
this.trigger('afterApplyGroupFlags', group);
};
/**
* Clears all errors markers
* @param {Node} [node] default is root Group
*/
QueryBuilder.prototype.clearErrors = function(node) {
node = node || this.model.root;
if (!node) {
return;
}
node.error = null;
if (node instanceof Group) {
node.each(function(rule) {
rule.error = null;
}, function(group) {
this.clearErrors(group);
}, this);
}
};
/**
* Adds/Removes error on a Rule or Group
* @param {Node} node
* @fires QueryBuilder.changer:displayError
* @private
*/
QueryBuilder.prototype.updateError = function(node) {
if (this.settings.display_errors) {
if (node.error === null) {
node.$el.removeClass('has-error');
}
else {
var errorMessage = this.translate('errors', node.error[0]);
errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
/**
* Modifies an error message before display
* @event changer:displayError
* @memberof QueryBuilder
* @param {string} errorMessage - the error message (translated and formatted)
* @param {array} error - the raw error array (error code and optional arguments)
* @param {Node} node
* @returns {string}
*/
errorMessage = this.change('displayError', errorMessage, node.error, node);
node.$el.addClass('has-error')
.find(QueryBuilder.selectors.error_container).eq(0)
.attr('title', errorMessage);
}
}
};
/**
* Triggers a validation error event
* @param {Node} node
* @param {string|array} error
* @param {*} value
* @fires QueryBuilder.validationError
* @private
*/
QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
if (!$.isArray(error)) {
error = [error];
}
/**
* Fired when a validation error occurred, can be prevented
* @event validationError
* @memberof QueryBuilder
* @param {Node} node
* @param {string} error
* @param {*} value
*/
var e = this.trigger('validationError', node, error, value);
if (!e.isDefaultPrevented()) {
node.error = error;
}
};