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/portal/static/src/js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/portal/static/src/js')
| -rw-r--r-- | addons/portal/static/src/js/portal.js | 245 | ||||
| -rw-r--r-- | addons/portal/static/src/js/portal_chatter.js | 311 | ||||
| -rw-r--r-- | addons/portal/static/src/js/portal_composer.js | 173 | ||||
| -rw-r--r-- | addons/portal/static/src/js/portal_sidebar.js | 75 | ||||
| -rw-r--r-- | addons/portal/static/src/js/portal_signature.js | 197 |
5 files changed, 1001 insertions, 0 deletions
diff --git a/addons/portal/static/src/js/portal.js b/addons/portal/static/src/js/portal.js new file mode 100644 index 00000000..610e58b5 --- /dev/null +++ b/addons/portal/static/src/js/portal.js @@ -0,0 +1,245 @@ +odoo.define('portal.portal', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +const Dialog = require('web.Dialog'); +const {_t, qweb} = require('web.core'); +const ajax = require('web.ajax'); + +publicWidget.registry.portalDetails = publicWidget.Widget.extend({ + selector: '.o_portal_details', + events: { + 'change select[name="country_id"]': '_onCountryChange', + }, + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + + this.$state = this.$('select[name="state_id"]'); + this.$stateOptions = this.$state.filter(':enabled').find('option:not(:first)'); + this._adaptAddressForm(); + + return def; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptAddressForm: function () { + var $country = this.$('select[name="country_id"]'); + var countryID = ($country.val() || 0); + this.$stateOptions.detach(); + var $displayedState = this.$stateOptions.filter('[data-country_id=' + countryID + ']'); + var nb = $displayedState.appendTo(this.$state).show().length; + this.$state.parent().toggle(nb >= 1); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCountryChange: function () { + this._adaptAddressForm(); + }, +}); + +publicWidget.registry.PortalHomeCounters = publicWidget.Widget.extend({ + selector: '.o_portal_my_home', + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + this._updateCounters(); + return def; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + async _updateCounters(elem) { + const numberRpc = 3; + const needed = this.$('[data-placeholder_count]') + .map((i, o) => $(o).data('placeholder_count')) + .toArray(); + const counterByRpc = Math.ceil(needed.length / numberRpc); // max counter, last can be less + + const proms = [...Array(Math.min(numberRpc, needed.length)).keys()].map(async i => { + await this._rpc({ + route: "/my/counters", + params: { + counters: needed.slice(i * counterByRpc, (i + 1) * counterByRpc) + }, + }).then(data => { + Object.keys(data).map(k => this.$("[data-placeholder_count='" + k + "']").text(data[k])); + }); + }); + return Promise.all(proms); + }, +}); + +publicWidget.registry.portalSearchPanel = publicWidget.Widget.extend({ + selector: '.o_portal_search_panel', + events: { + 'click .search-submit': '_onSearchSubmitClick', + 'click .dropdown-item': '_onDropdownItemClick', + 'keyup input[name="search"]': '_onSearchInputKeyup', + }, + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + this._adaptSearchLabel(this.$('.dropdown-item.active')); + return def; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptSearchLabel: function (elem) { + var $label = $(elem).clone(); + $label.find('span.nolabel').remove(); + this.$('input[name="search"]').attr('placeholder', $label.text().trim()); + }, + /** + * @private + */ + _search: function () { + var search = $.deparam(window.location.search.substring(1)); + search['search_in'] = this.$('.dropdown-item.active').attr('href').replace('#', ''); + search['search'] = this.$('input[name="search"]').val(); + window.location.search = $.param(search); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onSearchSubmitClick: function () { + this._search(); + }, + /** + * @private + */ + _onDropdownItemClick: function (ev) { + ev.preventDefault(); + var $item = $(ev.currentTarget); + $item.closest('.dropdown-menu').find('.dropdown-item').removeClass('active'); + $item.addClass('active'); + + this._adaptSearchLabel(ev.currentTarget); + }, + /** + * @private + */ + _onSearchInputKeyup: function (ev) { + if (ev.keyCode === $.ui.keyCode.ENTER) { + this._search(); + } + }, +}); + +/** + * Wraps an RPC call in a check for the result being an identity check action + * descriptor. If no such result is found, just returns the wrapped promise's + * result as-is; otherwise shows an identity check dialog and resumes the call + * on success. + * + * Warning: does not in and of itself trigger an identity check, a promise which + * never triggers and identity check internally will do nothing of use. + * + * @param {Function} rpc Widget#_rpc bound do the widget + * @param {Promise} wrapped promise to check for an identity check request + * @returns {Promise} result of the original call + */ +function handleCheckIdentity(rpc, wrapped) { + return wrapped.then((r) => { + if (!_.isMatch(r, {type: 'ir.actions.act_window', res_model: 'res.users.identitycheck'})) { + return r; + } + const check_id = r.res_id; + return ajax.loadXML('/portal/static/src/xml/portal_security.xml', qweb).then(() => new Promise((resolve, reject) => { + const d = new Dialog(null, { + title: _t("Security Control"), + $content: qweb.render('portal.identitycheck'), + buttons: [{ + text: _t("Confirm Password"), classes: 'btn btn-primary', + // nb: if click & close, waits for click to resolve before closing + click() { + const password_input = this.el.querySelector('[name=password]'); + if (!password_input.reportValidity()) { + password_input.classList.add('is-invalid'); + return; + } + return rpc({ + model: 'res.users.identitycheck', + method: 'write', + args: [check_id, {password: password_input.value}] + }).then(() => rpc({ + model: 'res.users.identitycheck', + method: 'run_check', + args: [check_id] + })).then((r) => { + this.close(); + resolve(r); + }, (err) => { + err.event.preventDefault(); // suppress crashmanager + password_input.classList.add('is-invalid'); + password_input.setCustomValidity(_t("Check failed")); + password_input.reportValidity(); + }); + } + }, { + text: _t('Cancel'), close: true + }, { + text: _t('Forgot password?'), classes: 'btn btn-link', + click() { window.location.href = "/web/reset_password/"; } + }] + }).on('close', null, () => { + // unlink wizard object? + reject(); + }); + d.opened(() => { + const pw = d.el.querySelector('[name="password"]'); + pw.focus(); + pw.addEventListener('input', () => { + pw.classList.remove('is-invalid'); + pw.setCustomValidity(''); + }); + d.el.addEventListener('submit', (e) => { + e.preventDefault(); + d.$footer.find('.btn-primary').click(); + }); + }); + d.open(); + })); + }); +} +return { + handleCheckIdentity, +} +}); diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js new file mode 100644 index 00000000..134efbf6 --- /dev/null +++ b/addons/portal/static/src/js/portal_chatter.js @@ -0,0 +1,311 @@ +odoo.define('portal.chatter', function (require) { +'use strict'; + +var core = require('web.core'); +const dom = require('web.dom'); +var publicWidget = require('web.public.widget'); +var time = require('web.time'); +var portalComposer = require('portal.composer'); + +var qweb = core.qweb; +var _t = core._t; + +/** + * Widget PortalChatter + * + * - Fetch message fron controller + * - Display chatter: pager, total message, composer (according to access right) + * - Provider API to filter displayed messages + */ +var PortalChatter = publicWidget.Widget.extend({ + template: 'portal.Chatter', + xmlDependencies: ['/portal/static/src/xml/portal_chatter.xml'], + events: { + 'click .o_portal_chatter_pager_btn': '_onClickPager', + 'click .o_portal_chatter_js_is_internal': 'async _onClickUpdateIsInternal', + }, + + /** + * @constructor + */ + init: function (parent, options) { + var self = this; + this.options = {}; + this._super.apply(this, arguments); + + // underscorize the camelcased option keys + _.each(options, function (val, key) { + self.options[_.str.underscored(key)] = val; + }); + // set default options + this.options = _.defaults(this.options, { + 'allow_composer': true, + 'display_composer': false, + 'csrf_token': odoo.csrf_token, + 'message_count': 0, + 'pager_step': 10, + 'pager_scope': 5, + 'pager_start': 1, + 'is_user_public': true, + 'is_user_employee': false, + 'is_user_publisher': false, + 'hash': false, + 'pid': false, + 'domain': [], + }); + + this.set('messages', []); + this.set('message_count', this.options['message_count']); + this.set('pager', {}); + this.set('domain', this.options['domain']); + this._currentPage = this.options['pager_start']; + }, + /** + * @override + */ + willStart: function () { + return Promise.all([ + this._super.apply(this, arguments), + this._chatterInit() + ]); + }, + /** + * @override + */ + start: function () { + // bind events + this.on("change:messages", this, this._renderMessages); + this.on("change:message_count", this, function () { + this._renderMessageCount(); + this.set('pager', this._pager(this._currentPage)); + }); + this.on("change:pager", this, this._renderPager); + this.on("change:domain", this, this._onChangeDomain); + // set options and parameters + this.set('message_count', this.options['message_count']); + this.set('messages', this.preprocessMessages(this.result['messages'])); + + + var defs = []; + defs.push(this._super.apply(this, arguments)); + + // instanciate and insert composer widget + if (this.options['display_composer']) { + this._composer = new portalComposer.PortalComposer(this, this.options); + defs.push(this._composer.replace(this.$('.o_portal_chatter_composer'))); + } + + return Promise.all(defs); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Fetch the messages and the message count from the server for the + * current page and current domain. + * + * @param {Array} domain + * @returns {Promise} + */ + messageFetch: function (domain) { + var self = this; + return this._rpc({ + route: '/mail/chatter_fetch', + params: self._messageFetchPrepareParams(), + }).then(function (result) { + self.set('messages', self.preprocessMessages(result['messages'])); + self.set('message_count', result['message_count']); + }); + }, + /** + * Update the messages format + * + * @param {Array<Object>} + * @returns {Array} + */ + preprocessMessages: function (messages) { + _.each(messages, function (m) { + m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id); + m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a')); + }); + return messages; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Deferred} + */ + _chatterInit: function () { + var self = this; + return this._rpc({ + route: '/mail/chatter_init', + params: this._messageFetchPrepareParams() + }).then(function (result) { + self.result = result; + self.options = _.extend(self.options, self.result['options'] || {}); + return result; + }); + }, + /** + * Change the current page by refreshing current domain + * + * @private + * @param {Number} page + * @param {Array} domain + */ + _changeCurrentPage: function (page, domain) { + this._currentPage = page; + var d = domain ? domain : _.clone(this.get('domain')); + this.set('domain', d); // trigger fetch message + }, + _messageFetchPrepareParams: function () { + var self = this; + var data = { + 'res_model': this.options['res_model'], + 'res_id': this.options['res_id'], + 'limit': this.options['pager_step'], + 'offset': (this._currentPage - 1) * this.options['pager_step'], + 'allow_composer': this.options['allow_composer'], + }; + // add token field to allow to post comment without being logged + if (self.options['token']) { + data['token'] = self.options['token']; + } + // add domain + if (this.get('domain')) { + data['domain'] = this.get('domain'); + } + return data; + }, + /** + * Generate the pager data for the given page number + * + * @private + * @param {Number} page + * @returns {Object} + */ + _pager: function (page) { + page = page || 1; + var total = this.get('message_count'); + var scope = this.options['pager_scope']; + var step = this.options['pager_step']; + + // Compute Pager + var pageCount = Math.ceil(parseFloat(total) / step); + + page = Math.max(1, Math.min(parseInt(page), pageCount)); + scope -= 1; + + var pmin = Math.max(page - parseInt(Math.floor(scope / 2)), 1); + var pmax = Math.min(pmin + scope, pageCount); + + if (pmax - scope > 0) { + pmin = pmax - scope; + } else { + pmin = 1; + } + + var pages = []; + _.each(_.range(pmin, pmax + 1), function (index) { + pages.push(index); + }); + + return { + "page_count": pageCount, + "offset": (page - 1) * step, + "page": page, + "page_start": pmin, + "page_previous": Math.max(pmin, page - 1), + "page_next": Math.min(pmax, page + 1), + "page_end": pmax, + "pages": pages + }; + }, + _renderMessages: function () { + this.$('.o_portal_chatter_messages').html(qweb.render("portal.chatter_messages", {widget: this})); + }, + _renderMessageCount: function () { + this.$('.o_message_counter').replaceWith(qweb.render("portal.chatter_message_count", {widget: this})); + }, + _renderPager: function () { + this.$('.o_portal_chatter_pager').replaceWith(qweb.render("portal.pager", {widget: this})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onChangeDomain: function () { + var self = this; + this.messageFetch().then(function () { + var p = self._currentPage; + self.set('pager', self._pager(p)); + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickPager: function (ev) { + ev.preventDefault(); + var page = $(ev.currentTarget).data('page'); + this._changeCurrentPage(page); + }, + + /** + * Toggle is_internal state of message. Update both node data and + * classes to ensure DOM is updated accordingly to RPC call result. + * @private + * @returns {Promise} + */ + _onClickUpdateIsInternal: function (ev) { + ev.preventDefault(); + + var $elem = $(ev.currentTarget); + return this._rpc({ + route: '/mail/update_is_internal', + params: { + message_id: $elem.data('message-id'), + is_internal: ! $elem.data('is-internal'), + }, + }).then(function (result) { + $elem.data('is-internal', result); + if (result === true) { + $elem.addClass('o_portal_message_internal_on'); + $elem.removeClass('o_portal_message_internal_off'); + } else { + $elem.addClass('o_portal_message_internal_off'); + $elem.removeClass('o_portal_message_internal_on'); + } + }); + }, +}); + +publicWidget.registry.portalChatter = publicWidget.Widget.extend({ + selector: '.o_portal_chatter', + + /** + * @override + */ + async start() { + const proms = [this._super.apply(this, arguments)]; + const chatter = new PortalChatter(this, this.$el.data()); + proms.push(chatter.appendTo(this.$el)); + await Promise.all(proms); + // scroll to the right place after chatter loaded + if (window.location.hash === `#${this.el.id}`) { + dom.scrollTo(this.el, {duration: 0}); + } + }, +}); + +return { + PortalChatter: PortalChatter, +}; +}); diff --git a/addons/portal/static/src/js/portal_composer.js b/addons/portal/static/src/js/portal_composer.js new file mode 100644 index 00000000..68fb9189 --- /dev/null +++ b/addons/portal/static/src/js/portal_composer.js @@ -0,0 +1,173 @@ +odoo.define('portal.composer', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var publicWidget = require('web.public.widget'); + +var qweb = core.qweb; +var _t = core._t; + +/** + * Widget PortalComposer + * + * Display the composer (according to access right) + * + */ +var PortalComposer = publicWidget.Widget.extend({ + template: 'portal.Composer', + xmlDependencies: ['/portal/static/src/xml/portal_chatter.xml'], + events: { + 'change .o_portal_chatter_file_input': '_onFileInputChange', + 'click .o_portal_chatter_attachment_btn': '_onAttachmentButtonClick', + 'click .o_portal_chatter_attachment_delete': 'async _onAttachmentDeleteClick', + 'click .o_portal_chatter_composer_btn': 'async _onSubmitButtonClick', + }, + + /** + * @constructor + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.options = _.defaults(options || {}, { + 'allow_composer': true, + 'display_composer': false, + 'csrf_token': odoo.csrf_token, + 'token': false, + 'res_model': false, + 'res_id': false, + }); + this.attachments = []; + }, + /** + * @override + */ + start: function () { + var self = this; + this.$attachmentButton = this.$('.o_portal_chatter_attachment_btn'); + this.$fileInput = this.$('.o_portal_chatter_file_input'); + this.$sendButton = this.$('.o_portal_chatter_composer_btn'); + this.$attachments = this.$('.o_portal_chatter_composer_form .o_portal_chatter_attachments'); + this.$attachmentIds = this.$('.o_portal_chatter_attachment_ids'); + this.$attachmentTokens = this.$('.o_portal_chatter_attachment_tokens'); + + return this._super.apply(this, arguments).then(function () { + if (self.options.default_attachment_ids) { + self.attachments = self.options.default_attachment_ids || []; + _.each(self.attachments, function(attachment) { + attachment.state = 'done'; + }); + self._updateAttachments(); + } + return Promise.resolve(); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAttachmentButtonClick: function () { + this.$fileInput.click(); + }, + /** + * @private + * @param {Event} ev + * @returns {Promise} + */ + _onAttachmentDeleteClick: function (ev) { + var self = this; + var attachmentId = $(ev.currentTarget).closest('.o_portal_chatter_attachment').data('id'); + var accessToken = _.find(this.attachments, {'id': attachmentId}).access_token; + ev.preventDefault(); + ev.stopPropagation(); + + this.$sendButton.prop('disabled', true); + + return this._rpc({ + route: '/portal/attachment/remove', + params: { + 'attachment_id': attachmentId, + 'access_token': accessToken, + }, + }).then(function () { + self.attachments = _.reject(self.attachments, {'id': attachmentId}); + self._updateAttachments(); + self.$sendButton.prop('disabled', false); + }); + }, + /** + * @private + * @returns {Promise} + */ + _onFileInputChange: function () { + var self = this; + + this.$sendButton.prop('disabled', true); + + return Promise.all(_.map(this.$fileInput[0].files, function (file) { + return new Promise(function (resolve, reject) { + var data = { + 'name': file.name, + 'file': file, + 'res_id': self.options.res_id, + 'res_model': self.options.res_model, + 'access_token': self.options.token, + }; + ajax.post('/portal/attachment/add', data).then(function (attachment) { + attachment.state = 'pending'; + self.attachments.push(attachment); + self._updateAttachments(); + resolve(); + }).guardedCatch(function (error) { + self.displayNotification({ + message: _.str.sprintf(_t("Could not save file <strong>%s</strong>"), + _.escape(file.name)), + type: 'warning', + sticky: true, + }); + resolve(); + }); + }); + })).then(function () { + // ensures any selection triggers a change, even if the same files are selected again + self.$fileInput[0].value = null; + self.$sendButton.prop('disabled', false); + }); + }, + /** + * Returns a Promise that is never resolved to prevent sending the form + * twice when clicking twice on the button, in combination with the `async` + * in the event definition. + * + * @private + * @returns {Promise} + */ + _onSubmitButtonClick: function () { + return new Promise(function (resolve, reject) {}); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _updateAttachments: function () { + this.$attachmentIds.val(_.pluck(this.attachments, 'id')); + this.$attachmentTokens.val(_.pluck(this.attachments, 'access_token')); + this.$attachments.html(qweb.render('portal.Chatter.Attachments', { + attachments: this.attachments, + showDelete: true, + })); + }, +}); + +return { + PortalComposer: PortalComposer, +}; +}); diff --git a/addons/portal/static/src/js/portal_sidebar.js b/addons/portal/static/src/js/portal_sidebar.js new file mode 100644 index 00000000..d081ddec --- /dev/null +++ b/addons/portal/static/src/js/portal_sidebar.js @@ -0,0 +1,75 @@ +odoo.define('portal.PortalSidebar', function (require) { +'use strict'; + +var core = require('web.core'); +var publicWidget = require('web.public.widget'); +var time = require('web.time'); +var session = require('web.session'); + +var _t = core._t; + +var PortalSidebar = publicWidget.Widget.extend({ + /** + * @override + */ + start: function () { + this._setDelayLabel(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------------- + + /** + * Set the due/delay information according to the given date + * like : <span class="o_portal_sidebar_timeago" t-att-datetime="invoice.date_due"/> + * + * @private + */ + _setDelayLabel: function () { + var $sidebarTimeago = this.$el.find('.o_portal_sidebar_timeago'); + _.each($sidebarTimeago, function (el) { + var dateTime = moment(time.auto_str_to_date($(el).attr('datetime'))), + today = moment().startOf('day'), + diff = dateTime.diff(today, 'days', true), + displayStr; + + session.is_bound.then(function (){ + if (diff === 0) { + displayStr = _t('Due today'); + } else if (diff > 0) { + // Workaround: force uniqueness of these two translations. We use %1d because the string + // with %d is already used in mail and mail's translations are not sent to the frontend. + displayStr = _.str.sprintf(_t('Due in %1d days'), Math.abs(diff)); + } else { + displayStr = _.str.sprintf(_t('%1d days overdue'), Math.abs(diff)); + } + $(el).text(displayStr); + }); + }); + }, + /** + * @private + * @param {string} href + */ + _printIframeContent: function (href) { + // due to this issue : https://bugzilla.mozilla.org/show_bug.cgi?id=911444 + // open a new window with pdf for print in Firefox (in other system: http://printjs.crabbly.com) + if ($.browser.mozilla) { + window.open(href, '_blank'); + return; + } + if (!this.printContent) { + this.printContent = $('<iframe id="print_iframe_content" src="' + href + '" style="display:none"></iframe>'); + this.$el.append(this.printContent); + this.printContent.on('load', function () { + $(this).get(0).contentWindow.print(); + }); + } else { + this.printContent.get(0).contentWindow.print(); + } + }, +}); +return PortalSidebar; +}); diff --git a/addons/portal/static/src/js/portal_signature.js b/addons/portal/static/src/js/portal_signature.js new file mode 100644 index 00000000..eac46519 --- /dev/null +++ b/addons/portal/static/src/js/portal_signature.js @@ -0,0 +1,197 @@ +odoo.define('portal.signature_form', function (require) { +'use strict'; + +var core = require('web.core'); +var publicWidget = require('web.public.widget'); +var NameAndSignature = require('web.name_and_signature').NameAndSignature; +var qweb = core.qweb; + +var _t = core._t; + +/** + * This widget is a signature request form. It uses + * @see NameAndSignature for the input fields, adds a submit + * button, and handles the RPC to save the result. + */ +var SignatureForm = publicWidget.Widget.extend({ + template: 'portal.portal_signature', + xmlDependencies: ['/portal/static/src/xml/portal_signature.xml'], + events: { + 'click .o_portal_sign_submit': 'async _onClickSignSubmit', + }, + custom_events: { + 'signature_changed': '_onChangeSignature', + }, + + /** + * Overridden to allow options. + * + * @constructor + * @param {Widget} parent + * @param {Object} options + * @param {string} options.callUrl - make RPC to this url + * @param {string} [options.sendLabel='Accept & Sign'] - label of the + * send button + * @param {Object} [options.rpcParams={}] - params for the RPC + * @param {Object} [options.nameAndSignatureOptions={}] - options for + * @see NameAndSignature.init() + */ + init: function (parent, options) { + this._super.apply(this, arguments); + + this.csrf_token = odoo.csrf_token; + + this.callUrl = options.callUrl || ''; + this.rpcParams = options.rpcParams || {}; + this.sendLabel = options.sendLabel || _t("Accept & Sign"); + + this.nameAndSignature = new NameAndSignature(this, + options.nameAndSignatureOptions || {}); + }, + /** + * Overridden to get the DOM elements + * and to insert the name and signature. + * + * @override + */ + start: function () { + var self = this; + this.$confirm_btn = this.$('.o_portal_sign_submit'); + this.$controls = this.$('.o_portal_sign_controls'); + var subWidgetStart = this.nameAndSignature.replace(this.$('.o_web_sign_name_and_signature')); + return Promise.all([subWidgetStart, this._super.apply(this, arguments)]).then(function () { + self.nameAndSignature.resetSignature(); + }); + }, + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Focuses the name. + * + * @see NameAndSignature.focusName(); + */ + focusName: function () { + this.nameAndSignature.focusName(); + }, + /** + * Resets the signature. + * + * @see NameAndSignature.resetSignature(); + */ + resetSignature: function () { + return this.nameAndSignature.resetSignature(); + }, + + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- + + /** + * Handles click on the submit button. + * + * This will get the current name and signature and validate them. + * If they are valid, they are sent to the server, and the reponse is + * handled. If they are invalid, it will display the errors to the user. + * + * @private + * @param {Event} ev + * @returns {Deferred} + */ + _onClickSignSubmit: function (ev) { + var self = this; + ev.preventDefault(); + + if (!this.nameAndSignature.validateSignature()) { + return; + } + + var name = this.nameAndSignature.getName(); + var signature = this.nameAndSignature.getSignatureImage()[1]; + + return this._rpc({ + route: this.callUrl, + params: _.extend(this.rpcParams, { + 'name': name, + 'signature': signature, + }), + }).then(function (data) { + if (data.error) { + self.$('.o_portal_sign_error_msg').remove(); + self.$controls.prepend(qweb.render('portal.portal_signature_error', {widget: data})); + } else if (data.success) { + var $success = qweb.render('portal.portal_signature_success', {widget: data}); + self.$el.empty().append($success); + } + if (data.force_refresh) { + if (data.redirect_url) { + window.location = data.redirect_url; + } else { + window.location.reload(); + } + // no resolve if we reload the page + return new Promise(function () { }); + } + }); + }, + /** + * Toggles the submit button depending on the signature state. + * + * @private + */ + _onChangeSignature: function () { + var isEmpty = this.nameAndSignature.isSignatureEmpty(); + this.$confirm_btn.prop('disabled', isEmpty); + }, +}); + +publicWidget.registry.SignatureForm = publicWidget.Widget.extend({ + selector: '.o_portal_signature_form', + + /** + * @private + */ + start: function () { + var hasBeenReset = false; + + var callUrl = this.$el.data('call-url'); + var nameAndSignatureOptions = { + defaultName: this.$el.data('default-name'), + mode: this.$el.data('mode'), + displaySignatureRatio: this.$el.data('signature-ratio'), + signatureType: this.$el.data('signature-type'), + fontColor: this.$el.data('font-color') || 'black', + }; + var sendLabel = this.$el.data('send-label'); + + var form = new SignatureForm(this, { + callUrl: callUrl, + nameAndSignatureOptions: nameAndSignatureOptions, + sendLabel: sendLabel, + }); + + // Correctly set up the signature area if it is inside a modal + this.$el.closest('.modal').on('shown.bs.modal', function (ev) { + if (!hasBeenReset) { + // Reset it only the first time it is open to get correct + // size. After we want to keep its content on reopen. + hasBeenReset = true; + form.resetSignature(); + } else { + form.focusName(); + } + }); + + return Promise.all([ + this._super.apply(this, arguments), + form.appendTo(this.$el) + ]); + }, +}); + +return { + SignatureForm: SignatureForm, +}; +}); |
