flectra/addons/web/static/src/js/widgets/domain_selector.js
2018-01-16 02:34:37 -08:00

972 lines
35 KiB
JavaScript

flectra.define("web.DomainSelector", function (require) {
"use strict";
var core = require("web.core");
var datepicker = require("web.datepicker");
var Domain = require("web.Domain");
var field_utils = require ("web.field_utils");
var ModelFieldSelector = require("web.ModelFieldSelector");
var Widget = require("web.Widget");
var _t = core._t;
var _lt = core._lt;
// "child_of", "parent_of", "like", "not like", "=like", "=ilike"
// are only used if user entered them manually or if got from demo data
var operator_mapping = {
"=": "=",
"!=": _lt("not"),
">": ">",
"<": "<",
">=": ">=",
"<=": "<=",
"ilike": _lt("contains"),
"not ilike": _lt("not contains"),
"in": _lt("in"),
"not in": _lt("not in"),
"child_of": _lt("child of"),
"parent_of": _lt("parent of"),
"like": "like",
"not like": "not like",
"=like": "=like",
"=ilike": "=ilike",
// custom
"set": _lt("is set"),
"not set": _lt("is not set"),
};
/**
* Abstraction for widgets which can represent and allow edition of a domain.
*/
var DomainNode = Widget.extend({
events: {
// If click on the node add or delete button, notify the parent and let
// it handle the addition/removal
"click .o_domain_add_node_button": "_onAddButtonClick",
"click .o_domain_delete_node_button": "_onDeleteButtonClick",
// Handle visual feedback and animation
"mouseenter button": "_onButtonEntered",
"mouseleave button": "_onButtonLeft",
},
/**
* A DomainNode needs a model and domain to work. It can also receive a set
* of options.
*
* @param {Object} parent
* @param {string} model - the model name
* @param {Array|string} domain - the prefix representation of the domain
* @param {Object} [options] - an object with possible values:
* @param {boolean} [options.readonly=true] - true if is readonly
* @param {string[]} [options.operators=null]
* a list of available operators (null = all of supported ones)
* @param {boolean} [options.debugMode=false] - true if should be in debug
*
* @see ModelFieldSelector for other options
*/
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
this.model = model;
this.options = _.extend({
readonly: true,
operators: null,
debugMode: false,
}, options || {});
this.readonly = this.options.readonly;
this.debug = this.options.debugMode;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Should return if the node is representing a well-formed domain, whose
* field chains properly belong to the associated model.
*
* @abstract
* @returns {boolean}
*/
isValid: function () {},
/**
* Should return the prefix domain the widget is currently representing
* (an array).
*
* @abstract
* @returns {Array}
*/
getDomain: function () {},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the add button is clicked -> trigger_up an event to ask
* creation of a new child in its parent.
*
* @param {Event} e
*/
_onAddButtonClick: function (e) {
e.preventDefault();
e.stopPropagation();
this.trigger_up("add_node_clicked", {newBranch: !!$(e.currentTarget).data("branch"), child: this});
},
/**
* Called when the delete button is clicked -> trigger_up an event to ask
* deletion of this node from its parent.
*
* @param {Event} e
*/
_onDeleteButtonClick: function (e) {
e.preventDefault();
e.stopPropagation();
this.trigger_up("delete_node_clicked", {child: this});
},
/**
* Called when a "controlpanel" button is hovered -> add classes to the
* domain node to add animation effects.
*
* @param {Event} e
*/
_onButtonEntered: function (e) {
e.preventDefault();
e.stopPropagation();
var $target = $(e.currentTarget);
this.$el.toggleClass("o_hover_btns", $target.hasClass("o_domain_delete_node_button"));
this.$el.toggleClass("o_hover_add_node", $target.hasClass("o_domain_add_node_button"));
this.$el.toggleClass("o_hover_add_inset_node", !!$target.data("branch"));
},
/**
* Called when a "controlpanel" button is not hovered anymore -> remove
* classes from the domain node to stop animation effects.
*
* @param {Event} e
*/
_onButtonLeft: function (e) {
e.preventDefault();
e.stopPropagation();
this.$el.removeClass("o_hover_btns o_hover_add_node o_hover_add_inset_node");
},
});
/**
* DomainNode which can handle subdomains (a domain which is composed of
* multiple parts). It thus will be composed of other DomainTree instances
* and/or leaf parts of a domain (@see DomainLeaf).
*/
var DomainTree = DomainNode.extend({
template: "DomainTree",
events: _.extend({}, DomainNode.prototype.events, {
"click .o_domain_tree_operator_selector > ul > li > a": "_onOperatorChange",
}),
custom_events: {
// If a domain child sends a request to add a child or remove one, call
// the appropriate methods. Propagates the event until success.
"add_node_clicked": "_onNodeAdditionAsk",
"delete_node_clicked": "_onNodeDeletionAsk",
},
/**
* @constructor
* @see DomainNode.init
* The initialization of a DomainTree creates a "children" array attribute
* which will contain the the DomainNode children. It also deduces the
* operator from the domain.
* @see DomainTree._addFlattenedChildren
*/
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
try {
domain = Domain.prototype.stringToArray(domain);
} catch (err) {
// TODO: domain could contain `parent` for example, which is
// currently not handled by the DomainSelector
this.invalidDomain = true;
this.children = [];
return;
}
this._initialize(domain);
},
/**
* @see DomainNode.start
* @returns {Deferred}
*/
start: function () {
this._postRender();
return $.when(this._super.apply(this, arguments), this._renderChildrenTo(this.$childrenContainer));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @see DomainNode.isValid
* @returns {boolean}
*/
isValid: function () {
for (var i = 0 ; i < this.children.length ; i++) {
var cValid = this.children[i].isValid();
if (!cValid) {
return cValid;
}
}
return this._isValid;
},
/**
* @see DomainNode.getDomain
* @returns {Array}
*/
getDomain: function () {
var childDomains = [];
var nbChildren = 0;
_.each(this.children, function (child) {
var childDomain = child.getDomain();
if (childDomain.length) {
nbChildren++;
childDomains = childDomains.concat(child.getDomain());
}
});
var nbChildRequired = this.operator === "!" ? 1 : 2;
var operators = _.times(nbChildren - nbChildRequired + 1, _.constant(this.operator));
return operators.concat(childDomains);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Adds a domain part to the widget.
* -> trigger_up "domain_changed" if the child is added
*
* @private
* @param {Array} domain - the prefix-like domain to build and add to the
* widget
* @param {DomainNode} afterNode - the node after which the new domain part
* must be added (at the end if not given)
* @returns {boolean} true if the part was added
* false otherwise (the afterNode was not found)
*/
_addChild: function (domain, afterNode) {
var i = afterNode ? _.indexOf(this.children, afterNode) : this.children.length;
if (i < 0) return false;
this.children.splice(i+1, 0, instantiateNode(this, this.model, domain, this.options));
this.trigger_up("domain_changed", {child: this});
return true;
},
/**
* Adds a child which represents the given domain. If the child has children
* and that the child main domain operator is the same as the current widget
* one, the 2-children prefix hierarchy is then simplified by making the
* child's children the widget's own children.
*
* @private
* @param {Array|string} domain - the domain of the child to add
*/
_addFlattenedChildren: function (domain) {
var node = instantiateNode(this, this.model, domain, this.options);
if (node === null) {
return;
}
if (!node.children || node.operator !== this.operator) {
this.children.push(node);
return;
}
_.each(node.children, (function (child) {
child.setParent(this);
this.children.push(child);
}).bind(this));
node.destroy();
},
/**
* Changes the operator of the domain tree and notifies the parent if
* necessary (not silent).
*
* @private
* @param {string} operator - the new operator
* @param {boolean} silent - true if the parents should not be notified of
* the change
*/
_changeOperator: function (operator, silent) {
this.operator = operator;
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/**
* @see DomainTree.init
* @private
*/
_initialize: function (domain) {
this._isValid = true;
this.operator = domain[0];
this.children = [];
if (domain.length <= 1) {
return;
}
// Add flattened children by search the appropriate number of children
// in the rest of the domain (after the operator)
var nbLeafsToFind = 1;
for (var i = 1 ; i < domain.length ; i++) {
if (domain[i] === "&" || domain[i] === "|") {
nbLeafsToFind++;
} else if (domain[i] !== "!") {
nbLeafsToFind--;
}
if (!nbLeafsToFind) {
var partLeft = domain.slice(1, i+1);
var partRight = domain.slice(i+1);
if (partLeft.length) {
this._addFlattenedChildren(partLeft);
}
if (partRight.length) {
this._addFlattenedChildren(partRight);
}
break;
}
}
this._isValid = (nbLeafsToFind === 0);
// Mark "!" tree children so that they do not allow to add other
// children around them
if (this.operator === "!") {
this.children[0].noControlPanel = true;
}
},
/**
* @see DomainTree.start
* Initializes variables which depend on the rendered widget.
* @private
*/
_postRender: function () {
this.$childrenContainer = this.$("> .o_domain_node_children_container");
},
/**
* Removes a given child from the widget.
* -> trigger_up domain_changed if the child is removed
*
* @private
* @param {DomainNode} oldChild - the child instance to remove
* @returns {boolean} true if the child was removed, false otherwise (the
* widget does not own the child)
*/
_removeChild: function (oldChild) {
var i = _.indexOf(this.children, oldChild);
if (i < 0) return false;
this.children[i].destroy();
this.children.splice(i, 1);
this.trigger_up("domain_changed", {child: this});
return true;
},
/**
* @see DomainTree.start
* Appends the children domain node to the given node. This is used to
* render the children widget in a dummy element before adding them in the
* DOM, otherwhise they could be misordered as they rendering is not
* synchronous.
*
* @private
* @param {jQuery} $to - the jQuery node to which the children must be added
* @returns {Deferred}
*/
_renderChildrenTo: function ($to) {
var $div = $("<div/>");
return $.when.apply($, _.map(this.children, (function (child) {
return child.appendTo($div);
}).bind(this))).then((function () {
_.each(this.children, function (child) {
child.$el.appendTo($to); // Forced to do it this way so that the
// children are not misordered
});
}).bind(this));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the operator select value is changed -> change the internal
* operator state
*
* @param {Event} e
*/
_onOperatorChange: function (e) {
e.preventDefault();
e.stopPropagation();
this._changeOperator($(e.target).data("operator"));
},
/**
* Called when a node addition was asked -> add the new domain part if on
* the right node or let the propagation continue.
*
* @param {FlectraEvent} e
*/
_onNodeAdditionAsk: function (e) {
var domain = [["id", "=", 1]];
if (e.data.newBranch) {
domain = [this.operator === "&" ? "|" : "&"].concat(domain).concat(domain);
}
if (this._addChild(domain, e.data.child)) {
e.stopPropagation();
}
},
/**
* Called when a node deletion was asked -> remove the domain part if on
* the right node or let the propagation continue.
*
* @param {FlectraEvent} e
*/
_onNodeDeletionAsk: function (e) {
if (this._removeChild(e.data.child)) {
e.stopPropagation();
}
},
});
/**
* The DomainSelector widget can be used to build prefix char domain. It is the
* DomainTree specialization to use to have a fully working widget.
*
* Known limitations:
*
* - Some operators like "child_of", "parent_of", "like", "not like",
* "=like", "=ilike" will come only if you use them from demo data or
* debug input.
* - Some kind of domain can not be build right now
* e.g ("country_id", "in", [1,2,3]) but you can insert from debug input.
*/
var DomainSelector = DomainTree.extend({
template: "DomainSelector",
events: _.extend({}, DomainTree.prototype.events, {
"click .o_domain_add_first_node_button": "_onAddFirstButtonClick",
"change .o_domain_debug_input": "_onDebugInputChange",
}),
custom_events: _.extend({}, DomainTree.prototype.custom_events, {
domain_changed: "_onDomainChange",
}),
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.invalidDomain) {
var msg = _t("This domain is not supported.");
self.$el.html(msg);
}
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Changes the internal domain value and forces a reparsing and rerendering.
* If the internal domain value was already equal to the given one, this
* does nothing.
*
* @param {Array|string} domain
* @returns {Deferred} resolved when the rerendering is finished
*/
setDomain: function (domain) {
if (Domain.prototype.arrayToString(domain) === Domain.prototype.arrayToString(this.getDomain())) {
return $.when();
}
return this._redraw(domain);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @see DomainTree._initialize
*/
_initialize: function (domain) {
// Check if the domain starts with implicit "&" operators and make them
// explicit. As the DomainSelector is a specialization of a DomainTree,
// it is waiting for a tree and not a leaf. So [] and [A] will be made
// explicit with ["&"], ["&", A] so that tree parsing is made correctly.
// Note: the domain is considered to be a valid one
if (domain.length > 1) {
Domain.prototype.normalizeArray(domain);
} else {
domain = ["&"].concat(domain);
}
return this._super(domain);
},
/**
* @see DomainTree._postRender
* Warns the user if the domain is not valid after rendering.
*/
_postRender: function () {
this._super.apply(this, arguments);
// Display technical domain if in debug mode
this.$debugInput = this.$(".o_domain_debug_input");
if (this.$debugInput.length) {
this.$debugInput.val(Domain.prototype.arrayToString(this.getDomain()));
}
// Warn the user if the domain is not valid after rendering
if (!this._isValid) {
this.do_warn(
_t("Domain error"),
_t("The domain you entered is not properly formed")
);
}
},
/**
* This method is ugly but achieves the right behavior without flickering.
*
* @param {Array|string} domain
* @returns {Deferred}
*/
_redraw: function (domain) {
var oldChildren = this.children.slice();
this._initialize(domain || this.getDomain());
return this._renderChildrenTo($("<div/>")).then((function () {
this.renderElement();
this._postRender();
_.each(this.children, (function (child) { child.$el.appendTo(this.$childrenContainer); }).bind(this));
_.each(oldChildren, function (child) { child.destroy(); });
}).bind(this));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "add a filter" button is clicked -> adds a first domain
* node
*/
_onAddFirstButtonClick: function () {
this._addChild([["id", "=", 1]]);
},
/**
* Called when the debug input value is changed -> constructs the tree
* representation if valid or warn the user if invalid.
*
* @param {Event} e
*/
_onDebugInputChange: function (e) {
// When the debug input changes, the string prefix domain is read. If it
// is syntax-valid the widget is re-rendered and notifies the parents.
// If not, a warning is shown to the user and the input is ignored.
var domain;
try {
domain = Domain.prototype.stringToArray($(e.currentTarget).val());
} catch (err) { // If there is a syntax error, just ignore the change
this.do_warn(_t("Syntax error"), _t("The domain you entered is not properly formed"));
return;
}
this._redraw(domain).then((function () {
this.trigger_up("domain_changed", {child: this, alreadyRedrawn: true});
}).bind(this));
},
/**
* Called when a (child's) domain has changed -> redraw the entire tree
* representation if necessary
*
* @param {FlectraEvent} e
*/
_onDomainChange: function (e) {
// If a subdomain notifies that it underwent some modifications, the
// DomainSelector catches the message and performs a full re-rendering.
if (!e.data.alreadyRedrawn) {
this._redraw();
}
},
});
/**
* DomainNode which handles a domain which cannot be split in another
* subdomains, i.e. composed of a field chain, an operator and a value.
*/
var DomainLeaf = DomainNode.extend({
template: "DomainLeaf",
events: _.extend({}, DomainNode.prototype.events, {
"change .o_domain_leaf_operator_select": "_onOperatorSelectChange",
"change .o_domain_leaf_value_input": "_onValueInputChange",
// Handle the tags widget part (TODO should be an independant widget)
"click .o_domain_leaf_value_add_tag_button": "on_add_tag",
"keyup .o_domain_leaf_value_tags input": "on_add_tag",
"click .o_domain_leaf_value_remove_tag_button": "on_remove_tag",
}),
custom_events: {
"field_chain_changed": "_onFieldChainChange",
},
/**
* @see DomainNode.init
*/
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
var currentDomain = Domain.prototype.stringToArray(domain);
this.chain = currentDomain[0][0];
this.operator = currentDomain[0][1];
this.value = currentDomain[0][2];
this.operator_mapping = operator_mapping;
},
/**
* Prepares the information the rendering of the widget will need by
* pre-instantiating its internal field selector widget.
*
* @returns {Deferred}
*/
willStart: function () {
var defs = [this._super.apply(this, arguments)];
// In edit mode, instantiate a field selector. This is done here in
// willStart and prepared by appending it to a dummy element because the
// DomainLeaf rendering need some information which cannot be computed
// before the ModelFieldSelector is fully rendered (TODO).
this.fieldSelector = new ModelFieldSelector(
this,
this.model,
this.chain ? this.chain.split(".") : [],
this.options
);
defs.push(this.fieldSelector.appendTo($("<div/>")).then((function () {
var wDefs = [];
if (!this.readonly) {
// Set list of operators according to field type
var selectedField = this.fieldSelector.getSelectedField() || {};
this.operators = this._getOperatorsFromType(selectedField.type);
if (_.contains(["child_of", "parent_of", "like", "not like", "=like", "=ilike"], this.operator)) {
// In case user entered manually or from demo data
this.operators[this.operator] = operator_mapping[this.operator];
} else if (!this.operators[this.operator]) {
// In case the domain uses an unsupported operator for the
// field type
this.operators[this.operator] = "?";
}
// Set list of values according to field type
this.selectionChoices = null;
if (selectedField.type === "boolean") {
this.selectionChoices = [["1", _t("set (true)")], ["0", _t("not set (false)")]];
} else if (selectedField.type === "selection") {
this.selectionChoices = selectedField.selection;
}
// Adapt display value and operator for rendering
this.displayValue = this.value;
try {
if (selectedField && !selectedField.relation && !_.isArray(this.value)) {
this.displayValue = field_utils.format[selectedField.type](this.value, selectedField);
}
} catch (err) {/**/}
this.displayOperator = this.operator;
if (selectedField.type === "boolean") {
this.displayValue = this.value ? "1" : "0";
} else if ((this.operator === "!=" || this.operator === "=") && this.value === false) {
this.displayOperator = this.operator === "!=" ? "set" : "not set";
}
// TODO the value could be a m2o input, etc...
if (_.contains(["date", "datetime"], selectedField.type)) {
this.valueWidget = new (selectedField.type === "datetime" ? datepicker.DateTimeWidget : datepicker.DateWidget)(this);
wDefs.push(this.valueWidget.appendTo("<div/>").then((function () {
this.valueWidget.$el.addClass("o_domain_leaf_value_input");
this.valueWidget.setValue(moment(this.value));
this.valueWidget.on("datetime_changed", this, function () {
this._changeValue(this.valueWidget.getValue());
});
}).bind(this)));
}
return $.when.apply($, wDefs);
}
}).bind(this)));
return $.when.apply($, defs);
},
/**
* @see DomainNode.start
* Appends the prepared field selector and value widget.
*
* @returns {Deferred}
*/
start: function () {
this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_info, > .o_domain_leaf_edition")); // place the field selector
if (!this.readonly && this.valueWidget) { // In edit mode, place the value widget if any
this.$(".o_domain_leaf_value_input").replaceWith(this.valueWidget.$el);
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @see DomainNode.isValid
* @returns {boolean}
*/
isValid: function () {
return this.fieldSelector && this.fieldSelector.isValid();
},
/**
* @see DomainNode.getDomain
* @returns {Array}
*/
getDomain: function () {
return [[this.chain, this.operator, this.value]];
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Handles a field chain change in the domain. In that case, the operator
* should be adapted to a valid one for the new field and the value should
* also be adapted to the new field and/or operator.
*
* -> trigger_up domain_changed event to ask for a re-rendering (if not
* silent)
*
* @param {string[]} chain - the new field chain
* @param {boolean} silent - true if the method call should not trigger_up a
* domain_changed event
*/
_changeFieldChain: function (chain, silent) {
this.chain = chain.join(".");
this.fieldSelector.setChain(chain).then((function () {
if (!this.fieldSelector.isValid()) return;
var selectedField = this.fieldSelector.getSelectedField() || {};
var operators = this._getOperatorsFromType(selectedField.type);
if (operators[this.operator] === undefined) {
this._changeOperator("=", true);
}
this._changeValue(this.value, true);
if (!silent) this.trigger_up("domain_changed", {child: this});
}).bind(this));
},
/**
* Handles an operator change in the domain. In that case, the value should
* be adapted to a valid one for the new operator.
*
* -> trigger_up domain_changed event to ask for a re-rendering
* (if not silent)
*
* @param {string} operator - the new operator
* @param {boolean} silent - true if the method call should not trigger_up a
* domain_changed event
*/
_changeOperator: function (operator, silent) {
this.operator = operator;
if (_.contains(["set", "not set"], this.operator)) {
this.operator = this.operator === "not set" ? "=" : "!=";
this.value = false;
} else if (_.contains(["in", "not in"], this.operator)) {
this.value = _.isArray(this.value) ? this.value : this.value ? ("" + this.value).split(",") : [];
} else {
if (_.isArray(this.value)) {
this.value = this.value.join(",");
}
this._changeValue(this.value, true);
}
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/**
* Handles a formatted value change in the domain. In that case, the value
* should be adapted to a valid technical one.
*
* -> trigger_up "domain_changed" event to ask for a re-rendering (if not
* silent)
*
* @param {*} value - the new formatted value
* @param {boolean} silent - true if the method call should not trigger_up a
* domain_changed event
*/
_changeValue: function (value, silent) {
var couldNotParse = false;
var selectedField = this.fieldSelector.getSelectedField() || {};
try {
this.value = field_utils.parse[selectedField.type](value, selectedField);
} catch (err) {
this.value = value;
couldNotParse = true;
}
if (selectedField.type === "boolean") {
if (!_.isBoolean(this.value)) { // Convert boolean-like value to boolean
this.value = !!parseFloat(this.value);
}
} else if (selectedField.type === "selection") {
if (!_.some(selectedField.selection, (function (option) { return option[0] === this.value; }).bind(this))) {
this.value = selectedField.selection[0][0];
}
} else if (_.contains(["date", "datetime"], selectedField.type)) {
if (couldNotParse || _.isBoolean(this.value)) {
this.value = field_utils.parse[selectedField.type](field_utils.format[selectedField.type](moment())).toJSON(); // toJSON to get date with server format
} else {
this.value = this.value.toJSON(); // toJSON to get date with server format
}
} else {
// Never display "true" or "false" strings from boolean value
if (_.isBoolean(this.value)) {
this.value = "";
} else if (_.isObject(this.value) && !_.isArray(this.value)) { // Can be object if parsed to x2x representation
this.value = this.value.id || value || "";
}
}
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/**
* Returns the mapping of "technical operator" to "display operator value"
* of the operators which are available for the given field type.
*
* @private
* @param {string} type - the field type
* @returns {Object} a map of all associated operators and their label
*/
_getOperatorsFromType: function (type) {
var operators = {};
switch (type) {
case "boolean":
operators = {
"=": _t("is"),
"!=": _t("is not"),
};
break;
case "char":
case "text":
case "html":
operators = _.pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set", "in", "not in");
break;
case "many2many":
case "one2many":
case "many2one":
operators = _.pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set");
break;
case "integer":
case "float":
case "monetary":
operators = _.pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "ilike", "not ilike", "set", "not set");
break;
case "selection":
operators = _.pick(operator_mapping, "=", "!=", "set", "not set");
break;
case "date":
case "datetime":
operators = _.pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "set", "not set");
break;
default:
operators = _.extend({}, operator_mapping);
break;
}
if (this.options.operators) {
operators = _.pick.apply(_, [operators].concat(this.options.operators));
}
return operators;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the operator select value is change -> change the operator
* internal state and adapt
*
* @param {Event} e
*/
_onOperatorSelectChange: function (e) {
this._changeOperator($(e.currentTarget).val());
},
/**
* Called when the value input value is changed -> change the internal value
* state and adapt
*
* @param {Event} e
*/
_onValueInputChange: function (e) {
if (e.currentTarget !== e.target) return;
this._changeValue($(e.currentTarget).val());
},
/**
* Called when the field selector value is changed -> change the internal
* chain state and adapt
*
* @param {FlectraEvent} e
*/
_onFieldChainChange: function (e) {
this._changeFieldChain(e.data.chain);
},
// TODO The two following functions should be in an independant widget
on_add_tag: function (e) {
if (e.type === "keyup" && e.which !== $.ui.keyCode.ENTER) return;
if (!_.contains(["not in", "in"], this.operator)) return;
var values = _.isArray(this.value) ? this.value.slice() : [];
var $input = this.$(".o_domain_leaf_value_tags input");
var val = $input.val().trim();
if (val && values.indexOf(val) < 0) {
values.push(val);
_.defer(this._changeValue.bind(this, values));
$input.focus();
}
},
on_remove_tag: function (e) {
var values = _.isArray(this.value) ? this.value.slice() : [];
var val = this.$(e.currentTarget).data("value");
var index = values.indexOf(val);
if (index >= 0) {
values.splice(index, 1);
_.defer(this._changeValue.bind(this, values));
}
},
});
/**
* Instantiates a DomainTree if the given domain contains several parts and a
* DomainLeaf if it only contains one part. Returns null otherwise.
*
* @param {Object} parent
* @param {string} model - the model name
* @param {Array|string} domain - the prefix representation of the domain
* @param {Object} options - @see DomainNode.init.options
* @returns {DomainTree|DomainLeaf|null}
*/
function instantiateNode(parent, model, domain, options) {
if (domain.length > 1) {
return new DomainTree(parent, model, domain, options);
} else if (domain.length === 1) {
return new DomainLeaf(parent, model, domain, options);
}
return null;
}
return DomainSelector;
});