diff options
| author | qsm-odoo <qsm@odoo.com> | 2017-01-03 17:29:20 +0100 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2017-01-03 18:38:03 +0100 |
| commit | e3f99885ca789b338a4cb9ab9a154db50dff500d (patch) | |
| tree | 5661dd542cc429b8b21b0ca2a5c5cf29ecffac08 /addons/web/static/src/js/widgets/model_field_selector.js | |
| parent | be58a61717e5ea270d280f145de7b7a54bd34745 (diff) | |
[IMP] web, *: add new Domain and Field selection widgets
* base_action_rule, gamification, mass_mailing
1) Create the independant DomainSelector widget
- Allow to build a prefix char domain
2) Replace old FieldCharDomain with a new FieldDomain which uses the
new DomainSelector widget
3) Create the independant ModelFieldSelector widget
- Allow to build a valid field chain for a specific model
(e.g 'partner_id.name' for a 'res.users')
- Used by DomainSelector widget
---
The DomainSelector widget uses a tree-like representation with
user-friendly buttons (ALL/ANY/+/-/...). Any domain can be built,
except those containing the "!" (not) operator or some operators
like "like", "child_of" (@see DomainSelector). However, if the
user enters these in the debug input, the widget can handle them.
Note: these widgets had to do unusual stuff the widget API does not
allow to handle efficiently right now but it will be adapted to the
new widget/field API in a further update.
Note2: the widgets also have possibility of other improvements like
supporting m2o input instantiation in DomainLeaf, etc.
---
This work is an adaptation of the original code made by @pga-odoo.
Diffstat (limited to 'addons/web/static/src/js/widgets/model_field_selector.js')
| -rw-r--r-- | addons/web/static/src/js/widgets/model_field_selector.js | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js new file mode 100644 index 00000000000..109b7ead7df --- /dev/null +++ b/addons/web/static/src/js/widgets/model_field_selector.js @@ -0,0 +1,318 @@ +odoo.define("web.ModelFieldSelector", function (require) { +"use strict"; + +var core = require("web.core"); +var Model = require("web.DataModel"); +var Widget = require("web.Widget"); + +/// The ModelFieldSelector widget can be used to select a particular field chain from a given model. +var ModelFieldSelector = Widget.extend({ + template: "FieldSelector", + events: { + // Handle popover opening and closing + "focusin input, .o_field_selector_popover": function (e) { + if (this._hidePopoverTimeout !== undefined) { + clearTimeout(this._hidePopoverTimeout); + this._hidePopoverTimeout = undefined; + return; + } + if (this.$input.is(e.currentTarget)) { + this.showPopover(); + } + }, + "focusout input, .o_field_selector_popover": function () { + if (this._hidePopoverTimeout !== undefined) { + return; + } + this._hidePopoverTimeout = setTimeout((function () { + this.hidePopover(); + this._hidePopoverTimeout = undefined; + }).bind(this), 100); + }, + "click .o_field_selector_close": function () { + if (this._hidePopoverTimeout !== undefined) { + clearTimeout(this._hidePopoverTimeout); + this._hidePopoverTimeout = undefined; + } + this.hidePopover(); + }, + + // Handle popover field navigation + "click .o_field_selector_prev_page": "goToPrevPage", + "click .o_field_selector_next_page": function (e) { + e.stopPropagation(); + this.goToNextPage(this._getLastPageField($(e.currentTarget).data("name"))); + }, + "click li.o_field_selector_select_button": function (e) { + this.selectField(this._getLastPageField($(e.currentTarget).data("name"))); + }, + + // Handle a direct change in the debug input + "change input": function() { + this.setChain(this.$input.val()); + this.validate(true); + this._prefill().then(this.displayPage.bind(this, "")); + this.trigger_up("field_chain_changed", {chain: this.chain}); + }, + + // Handle keyboard and mouse navigation to build the field chain + "mouseover li.o_field_selector_item": function (e) { + this.$("li.o_field_selector_item").removeClass("active"); + $(e.currentTarget).addClass("active"); + }, + "keydown": function (e) { + if (!this.$popover.is(":visible")) return; + var inputHasFocus = this.$input.is(":focus"); + + switch (e.which) { + case $.ui.keyCode.UP: + case $.ui.keyCode.DOWN: + e.preventDefault(); + var $active = this.$("li.o_field_selector_item.active"); + var $to = $active[e.which === $.ui.keyCode.DOWN ? "next" : "prev"](".o_field_selector_item"); + if ($to.length) { + $active.removeClass("active"); + $to.addClass("active"); + this.$popover.focus(); + + var $page = $to.closest(".o_field_selector_page"); + var full_height = $page.height(); + var el_position = $to.position().top; + var el_height = $to.outerHeight(); + var current_scroll = $page.scrollTop(); + if (el_position < 0) { + $page.scrollTop(current_scroll - el_height); + } else if (full_height < el_position + el_height) { + $page.scrollTop(current_scroll + el_height); + } + } + break; + case $.ui.keyCode.RIGHT: + if (inputHasFocus) break; + e.preventDefault(); + var name = this.$("li.o_field_selector_item.active").data("name"); + if (name) { + var field = this._getLastPageField(name); + if (field.relation) { + this.goToNextPage(field); + } + } + break; + case $.ui.keyCode.LEFT: + if (inputHasFocus) break; + e.preventDefault(); + this.goToPrevPage(); + break; + case $.ui.keyCode.ESCAPE: + e.stopPropagation(); + this.hidePopover(); + break; + case $.ui.keyCode.ENTER: + if (inputHasFocus) break; + e.preventDefault(); + this.selectField(this._getLastPageField(this.$("li.o_field_selector_item.active").data("name"))); + break; + } + }, + }, + /// The ModelFieldSelector requires a model and a initial field chain to work with. + /// @param model - a string with the model name (e.g. "res.partner") + /// @param chain - a string with the initial field chain (e.g. "company_id.name") + /// @param options - an object with several options: + /// - filters: an object which contains a series of filters to use + /// - debugMode: a boolean which is true if the widget is in debug mode + init: function (parent, model, chain, options) { + this._super.apply(this, arguments); + + this.model = model; + this.chain = chain; + this.options = _.extend({ + filters: {}, + debugMode: false, + }, options || {}); + + this.pages = []; + this.selectedField = false; + this.isSelected = true; + this.dirty = false; + + this.debug = this.options.debugMode; + }, + willStart: function () { + return $.when( + this._super.apply(this, arguments), + this._prefill() + ); + }, + start: function () { + this.$input = this.$("input"); + this.$popover = this.$(".o_field_selector_popover"); + this.displayPage(); + + return this._super.apply(this, arguments); + }, + /// The setChain method saves a new field chain string and displays it in the DOM input element. + /// @param chain - the new field chain string + setChain: function (chain) { + this.chain = chain; + this.$input.val(this.chain); + }, + /// The addChainNode method adds a field name to the current field chain. + /// @param fieldName - the new field name to add at the end of the current field chain + addChainNode: function (fieldName) { + this.dirty = true; + if (this.isSelected) { + this.removeChainNode(); + this.isSelected = false; + } + if (!this.valid) { + this.setChain(""); + this.validate(true); + } + this.setChain((this.chain ? (this.chain + ".") : "") + fieldName); + }, + /// The removeChainNode method removes the last field name at the end of the current field chain. + removeChainNode: function () { + this.setChain(this.chain.substring(0, this.chain.lastIndexOf("."))); + }, + /// The validate method toggles the valid status of the widget and display the error message if it + /// is not valid. + /// @param valid - a boolean which is true if the widget is valid + validate: function (valid) { + this.$(".o_field_selector_warning").toggleClass("hidden", valid); + this.valid = valid; + }, + /// The showPopover method shows the popover to select the field chain. It prepares the popover pages + /// before actually showing it. + showPopover: function () { + this._prefill().then((function () { + this.displayPage(); + this.$popover.removeClass("hidden"); + }).bind(this)); + }, + /// The hidePopover method closes the popover and mark the field as selected. If the field chain changed, + /// it notifies its parents. + hidePopover: function () { + this.$popover.addClass("hidden"); + this.isSelected = true; + if (this.dirty) { + this.trigger_up("field_chain_changed", {chain: this.chain}); + this.dirty = false; + } + }, + /// The private _prefill method prepares the popover by filling its pages according to the current field chain. + /// @return a deferred which is resolved once the last page is shown + _prefill: function () { + this.pages = []; + return this._pushPageData(this.model).then((function() { + return (this.chain ? processChain.call(this, this.chain.split(".").reverse()) : $.when()); + }).bind(this)); + + function processChain(chain) { + var field = this._getLastPageField(chain.pop()); + if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible + return this._pushPageData(field.relation).then(processChain.bind(this, chain)); + } else if (field && chain.length === 0) { // Last node fetched, save it + this.selectedField = field; + this.validate(true); + } else { // Wrong node chain + this.validate(false); + } + return $.when(); + } + }, + /// The private _pushPageData method gets the field of a particular model and adds them for the new + /// last popover page. + /// @param model - the model name whose fields have to be fetched + /// @return a deferred which is resolved once the fields have been added + _pushPageData: function (model) { + return fieldsCache.getFields(model, this.options.filters).then((function (fields) { + this.pages.push(fields); + }).bind(this)); + }, + /// The displayPage method shows the last page content of the popover. It also adapts the title according + /// to the previous page. + /// @param animation - an optional animation class to add to the page + displayPage: function (animation) { + this.$(".o_field_selector_prev_page").toggleClass("hidden", this.pages.length === 1); + + var page = _.last(this.pages); + var title = ""; + if (this.pages.length > 1) { + var chainParts = this.chain.split("."); + var prevField = _.findWhere(this.pages[this.pages.length - 2], { + name: this.isSelected ? chainParts[chainParts.length - 2] : _.last(chainParts), + }); + if (prevField) title = prevField.string; + } + this.$(".o_field_selector_popover_header .o_field_selector_title").text(title); + this.$(".o_field_selector_page").replaceWith(core.qweb.render("FieldSelector.page", {lines: page, animation: animation, debug: this.debug})); + }, + /// The goToPrevPage method removes the last page, adapts the field chain and displays the new last page. + goToPrevPage: function () { + if (this.pages.length <= 1) return; + this.pages.pop(); + this.removeChainNode(); + this.displayPage("o_animate_slide_left"); + }, + /// The goToNextPage method adds a new page to the popover following the given field relation and adapts + /// the chain node according to this given field. + /// @param field - the field to add to the chain node + goToNextPage: function (field) { + this.addChainNode(field.name); + this.selectedField = field; + this._pushPageData(field.relation).then(this.displayPage.bind(this, "o_animate_slide_right")); + }, + /// The selectField method selects the given field and adapts the chain node according to it. It also closes + /// the popover and thus notifies the parents about the change. + /// @param field - the field to select + selectField: function (field) { + this.addChainNode(field.name); + this.selectedField = field; + this.hidePopover(); + }, + /// The private _getLastPageField search a field in the last page by its name. + /// @return the field data (an object) found in the last popover page thanks to its name + _getLastPageField: function (name) { + return _.findWhere(_.last(this.pages), { + name: name, + }); + }, +}); + +/// Field Selector Cache +/// +/// * Stores fields per model used in field selector +/// * Apply filters on the fly +var fieldsCache = { + cache: {}, + cacheDefs: {}, + getFields: function (model, filters) { + return (this.cacheDefs[model] ? this.cacheDefs[model] : this.updateCache(model)).then((function () { + return this.filter(model, filters); + }).bind(this)); + }, + updateCache: function (model) { + this.cacheDefs[model] = new Model(model).call("fields_get", [ + false, + ["store", "searchable", "type", "string", "relation", "selection", "related"], + ]).then((function (fields) { + var field_data = []; + _.each(_.sortBy(_.keys(fields), function (f) { return fields[f].string; }), function (key) { + fields[key]["name"] = key; + field_data.push(fields[key]); + }); + this.cache[model] = field_data; + }).bind(this)); + return this.cacheDefs[model]; + }, + filter: function (model, filters) { + filters = _.defaults(filters || {}, {searchable: true}); + return _.filter(this.cache[model], function (f) { + return !filters.searchable || f.searchable; + }); + }, +}; + +return ModelFieldSelector; +}); |
