diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/widgets/domain_selector.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/widgets/domain_selector.js')
| -rw-r--r-- | addons/web/static/src/js/widgets/domain_selector.js | 987 |
1 files changed, 987 insertions, 0 deletions
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js new file mode 100644 index 00000000..9fe71ce1 --- /dev/null +++ b/addons/web/static/src/js/widgets/domain_selector.js @@ -0,0 +1,987 @@ +odoo.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("is not ="), + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "ilike": _lt("contains"), + "not ilike": _lt("does not contain"), + "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 {Array} [options.default] - default domain used when creating a + * new node + * @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 .dropdown-item": "_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) { + this._super.apply(this, arguments); + var parsedDomain = this._parseDomain(domain); + if (parsedDomain) { + this._initialize(parsedDomain); + } + }, + /** + * @see DomainNode.start + * @returns {Promise} + */ + start: function () { + this._postRender(); + return Promise.all([ + 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 {Promise} + */ + _renderChildrenTo: function ($to) { + var $div = $("<div/>"); + return Promise.all(_.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)); + }, + /** + * @param {string} domain + * @returns {Array[]} + */ + _parseDomain: function (domain) { + var parsedDomain = false; + try { + parsedDomain = Domain.prototype.stringToArray(domain); + this.invalidDomain = false; + } catch (err) { + // TODO: domain could contain `parent` for example, which is + // currently not handled by the DomainSelector + this.invalidDomain = true; + this.children = []; + } + return parsedDomain; + }, + + //-------------------------------------------------------------------------- + // 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 {OdooEvent} e + */ + _onNodeAdditionAsk: function (e) { + var domain = this.options.default || [["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 {OdooEvent} 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 {string} domain + * @returns {Promise} resolved when the rerendering is finished + */ + setDomain: function (domain) { + if (domain === Domain.prototype.arrayToString(this.getDomain())) { + return Promise.resolve(); + } + var parsedDomain = this._parseDomain(domain); + if (parsedDomain) { + return this._redraw(parsedDomain); + } + }, + + //-------------------------------------------------------------------------- + // 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(false, _t("Domain not supported")); + } + }, + /** + * This method is ugly but achieves the right behavior without flickering. + * + * @param {Array|string} domain + * @returns {Promise} + */ + _redraw: function (domain) { + var oldChildren = this.children.slice(); + this._initialize(domain || this.getDomain()); + return this._renderChildrenTo($("<div/>")).then((function () { + _.each(oldChildren, function (child) { child.destroy(); }); + this.renderElement(); + this._postRender(); + _.each(this.children, (function (child) { child.$el.appendTo(this.$childrenContainer); }).bind(this)); + }).bind(this)); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "add a filter" button is clicked -> adds a first domain + * node + */ + _onAddFirstButtonClick: function () { + this._addChild(this.options.default || [["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("Domain 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 {OdooEvent} 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 {Promise} + */ + 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 !== undefined ? this.chain.toString().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 Promise.all(wDefs); + } + }).bind(this))); + + return Promise.all(defs); + }, + /** + * @see DomainNode.start + * Appends the prepared field selector and value widget. + * + * @returns {Promise} + */ + 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 {OdooEvent} 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; +}); |
