odoo.define('partner.autocomplete.fieldchar', function (require) { 'use strict'; var basic_fields = require('web.basic_fields'); var core = require('web.core'); var field_registry = require('web.field_registry'); var AutocompleteMixin = require('partner.autocomplete.Mixin'); var QWeb = core.qweb; var FieldChar = basic_fields.FieldChar; /** * FieldChar extension to suggest existing companies when changing the company * name on a res.partner view (indeed, it is designed to change the "name", * "website" and "image" fields of records of this model). */ var FieldAutocomplete = FieldChar.extend(AutocompleteMixin, { className: 'o_field_partner_autocomplete', debounceSuggestions: 400, resetOnAnyFieldChange: true, jsLibs: [ '/partner_autocomplete/static/lib/jsvat.js' ], events: _.extend({}, FieldChar.prototype.events, { 'keyup': '_onKeyup', 'mousedown .o_partner_autocomplete_suggestion': '_onMousedown', 'focusout': '_onFocusout', 'mouseenter .o_partner_autocomplete_suggestion': '_onHoverDropdown', 'click .o_partner_autocomplete_suggestion': '_onSuggestionClicked', }), /** * @constructor * Prepares the basic rendering of edit mode by setting the root to be a * div.dropdown.open. * @see FieldChar.init */ init: function () { this._super.apply(this, arguments); // If the autocomplete is applied to vat field, only search valid vat number this.onlyVAT = this.name === 'vat'; if (this.mode === 'edit') { this.tagName = 'div'; this.className += ' dropdown open'; } if (this.debounceSuggestions > 0) { this._suggestCompanies = _.debounce(this._suggestCompanies.bind(this), this.debounceSuggestions); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Check if the autocomplete should be active * Active : * - only when creating new record * - on model res.partner and is_company=true * - on model res.company * * @returns {boolean} * @private */ _isActive: function () { return this.model === 'res.company' || ( this.model === 'res.partner' && this.record.data.is_company && !(this.record.data && this.record.data.id) ); }, /** * * @private */ _removeDropdown: function () { if (this.$dropdown) { this.$dropdown.remove(); this.$dropdown = undefined; } }, /** * Adds the element and prepares it. Note: the dropdown rendering * is handled outside of the rendering routine (but instead by reacting to * user input). * * @override * @private */ _renderEdit: function () { this.$el.empty(); // Prepare and add the input this._prepareInput().appendTo(this.$el); }, /** * Selects the given company suggestions by notifying changes to the view * for the "name", "website" and "image" fields. This is of course intended * to work only with the "res.partner" form view. * * @private * @param {Object} company */ _selectCompany: function (company) { var self = this; this._getCreateData(company).then(function (data) { if (data.logo) { var logoField = self.model === 'res.partner' ? 'image_1920' : 'logo'; data.company[logoField] = data.logo; } // Some fields are unnecessary in res.company if (self.model === 'res.company') { var fields = 'comment,child_ids,bank_ids,additional_info'.split(','); fields.forEach(function (field) { delete data.company[field]; }); } self._setOne2ManyField('bank_ids', data.company.bank_ids); delete data.company.bank_ids; self.trigger_up('field_changed', { dataPointID: self.dataPointID, changes: data.company, onSuccess: function () { // update the input's value directly if (self.onlyVAT) self.$input.val(self._formatValue(company.vat)); else self.$input.val(self._formatValue(company.name)); }, }); }); this._removeDropdown(); }, _setOne2ManyField: function (field, list) { var self = this; var viewType = this.record.viewType; if (list && this.record.fieldsInfo[viewType] && this.record.fieldsInfo[viewType][field]) { list.forEach(function (item) { var changes = {}; changes[field] = { operation: 'CREATE', data: item, }; self.trigger_up('field_changed', { dataPointID: self.dataPointID, changes: changes, }); }); } }, /** * Shows the dropdown with the suggestions. If one is * already opened, it removes the old one before rerendering the dropdown. * * @private */ _showDropdown: function () { this._removeDropdown(); if (this.suggestions.length > 0) { this.$dropdown = $(QWeb.render('partner_autocomplete.dropdown', { suggestions: this.suggestions, })); this.$dropdown.appendTo(this.$el); } }, /** * Shows suggestions according to the given value. * Note: this method is debounced (@see init). * * @private * @param {string} value - searched term */ _suggestCompanies: function (value) { var self = this; if (this._validateSearchTerm(value, this.onlyVAT) && this._isOnline()) { return this._autocomplete(value).then(function (suggestions) { if (suggestions && suggestions.length) { self.suggestions = suggestions; self._showDropdown(); } else { self._removeDropdown(); } }); } else { this._removeDropdown(); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Called on focusout -> removes the suggestions dropdown. * * @private */ _onFocusout: function () { this._removeDropdown(); }, /** * Called when hovering a suggestion in the dropdown -> sets it as active. * * @private * @param {Event} e */ _onHoverDropdown: function (e) { this.$dropdown.find('.active').removeClass('active'); $(e.currentTarget).parent().addClass('active'); }, /** * @override of FieldChar (called when the user is typing text) * Checks the value and shows suggestions according to * this value. * * @private */ _onInput: function () { this._super.apply(this, arguments); if (this._isActive()) { this._suggestCompanies(this.$input.val()); } }, /** * @override of FieldChar * Changes the "up" and "down" key behavior when the dropdown is opened (to * navigate through dropdown suggestions). * Triggered by keydown to execute the navigation multiple times when the * user keeps the "down" or "up" pressed. * * @private * @param {Event} e */ _onKeydown: function (e) { switch (e.which) { case $.ui.keyCode.UP: case $.ui.keyCode.DOWN: if (!this.$dropdown) { break; } e.preventDefault(); var $suggestions = this.$dropdown.children(); var $active = $suggestions.filter('.active'); var $to; if ($active.length) { $to = e.which === $.ui.keyCode.DOWN ? $active.next() : $active.prev(); } else { $to = $suggestions.first(); } if ($to.length) { $active.removeClass('active'); $to.addClass('active'); } return; } this._super.apply(this, arguments); }, /** * Called on keyup events to: * -> remove the suggestions dropdown when hitting the "escape" key * -> select the highlighted suggestion when hitting the "enter" key * * @private * @param {Event} e */ _onKeyup: function (e) { switch (e.which) { case $.ui.keyCode.ESCAPE: e.preventDefault(); this._removeDropdown(); break; case $.ui.keyCode.ENTER: if (!this.$dropdown) { break; } e.preventDefault(); var $active = this.$dropdown.find('.o_partner_autocomplete_suggestion.active'); if (!$active.length) { return; } this._selectCompany(this.suggestions[$active.data('index')]); break; } }, /** * Called on mousedown event on a suggestion -> prevent default * action so that the element does not lose the focus. * * @private * @param {Event} e */ _onMousedown: function (e) { e.preventDefault(); // prevent losing focus on suggestion click }, /** * Called when a dropdown suggestion is clicked -> trigger_up changes for * some fields in the view (not only this one) with the associated * data (@see _selectCompany). * * @private * @param {Event} e */ _onSuggestionClicked: function (e) { e.preventDefault(); this._selectCompany(this.suggestions[$(e.currentTarget).data('index')]); }, }); field_registry.add('field_partner_autocomplete', FieldAutocomplete); return FieldAutocomplete; });