diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index ea019834c15..ea75de8073d 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 import math +import os import re from werkzeug import urls @@ -186,6 +188,27 @@ class CustomerPortal(Controller): response.headers['X-Frame-Options'] = 'DENY' return response + @route(['/portal/sign/get_fonts'], type='json', auth='public') + def get_fonts(self): + """This route will return a list of base64 encoded fonts. + + Those fonts will be proposed to the user when creating a signature + using mode 'auto'. + + :return: base64 encoded fonts + :rtype: list + """ + current_dir = os.path.dirname(os.path.abspath(__file__)) + fonts_directory = os.path.join(current_dir, '..', 'static', 'font', 'sign') + font_filenames = sorted(os.listdir(fonts_directory)) + + fonts = [] + for filename in font_filenames: + font_file = open(os.path.join(fonts_directory, filename), 'rb') + font = base64.b64encode(font_file.read()) + fonts.append(font) + return fonts + def details_form_validate(self, data): error = dict() error_message = [] diff --git a/addons/portal/static/font/sign/Aspire-DemiBold.ttf b/addons/portal/static/font/sign/Aspire-DemiBold.ttf new file mode 100644 index 00000000000..d8cb1992b86 Binary files /dev/null and b/addons/portal/static/font/sign/Aspire-DemiBold.ttf differ diff --git a/addons/portal/static/font/sign/JennaSue.ttf b/addons/portal/static/font/sign/JennaSue.ttf new file mode 100644 index 00000000000..667a55112dd Binary files /dev/null and b/addons/portal/static/font/sign/JennaSue.ttf differ diff --git a/addons/portal/static/font/sign/Otto.ttf b/addons/portal/static/font/sign/Otto.ttf new file mode 100644 index 00000000000..9a2a51dadd2 Binary files /dev/null and b/addons/portal/static/font/sign/Otto.ttf differ diff --git a/addons/portal/static/font/sign/Pen_Frenzy.ttf b/addons/portal/static/font/sign/Pen_Frenzy.ttf new file mode 100644 index 00000000000..d9f72c59340 Binary files /dev/null and b/addons/portal/static/font/sign/Pen_Frenzy.ttf differ diff --git a/addons/portal/static/font/sign/Prophecy_Script.ttf b/addons/portal/static/font/sign/Prophecy_Script.ttf new file mode 100644 index 00000000000..57fd1b3647a Binary files /dev/null and b/addons/portal/static/font/sign/Prophecy_Script.ttf differ diff --git a/addons/portal/static/font/sign/khand.ttf b/addons/portal/static/font/sign/khand.ttf new file mode 100644 index 00000000000..f71b7493a3f Binary files /dev/null and b/addons/portal/static/font/sign/khand.ttf differ diff --git a/addons/portal/static/src/js/portal_name_and_signature.js b/addons/portal/static/src/js/portal_name_and_signature.js new file mode 100644 index 00000000000..20ce8d5ae22 --- /dev/null +++ b/addons/portal/static/src/js/portal_name_and_signature.js @@ -0,0 +1,589 @@ +odoo.define('portal.name_and_signature', function (require) { +'use strict'; + +var core = require('web.core'); +var Widget = require('web.Widget'); + +var _t = core._t; + +/** + * This widget allows the user to input his name and to draw his signature. + * Alternatively the signature can also be generated automatically based on + * the given name and a selected font, or loaded from an image file. + */ +var NameAndSignature = Widget.extend({ + template: 'portal.sign_name_and_signature', + xmlDependencies: ['/portal/static/src/xml/portal_name_and_signature.xml'], + events: { + // name + 'input .o_portal_sign_name_input': '_onInputSignName', + // signature + 'click .o_portal_sign_signature': '_onClickSignature', + // draw + 'click .o_portal_sign_draw_button': '_onClickSignDrawButton', + 'click .o_portal_sign_draw_clear a': '_onClickSignDrawClear', + // auto + 'click .o_portal_sign_auto_button': '_onClickSignAutoButton', + 'click .o_portal_sign_auto_select_style a': '_onClickSignAutoSelectStyle', + 'click .o_portal_sign_auto_font_selection a': '_onClickSignAutoFontSelection', + 'mouseover .o_portal_sign_auto_font_selection a': '_onMouseOverSignAutoFontSelection', + 'touchmove .o_portal_sign_auto_font_selection a': '_onTouchStartSignAutoFontSelection', + // load + 'click .o_portal_sign_load_button': '_onClickSignLoadButton', + 'change .o_portal_sign_load_file input': '_onChangeSignLoadInput', + }, + + /** + * Allows options. + * + * @constructor + * @param {Widget} parent + * @param {Object} [options={}] + * @param {number} [options.signatureRatio=3.0] - The ratio used when + * (re)computing the size of the signature (width = height * ratio) + * @param {string} [options.defaultName=''] - The default name of + * the signer. + * @param {string} [options.mode='draw'] - @see this.setMode + * @param {string} [options.signatureType='signature'] - The type of + * signature used in 'auto' mode. Can be one of the following values: + * - 'signature': it will adapt the characters width to fit the whole + * text in the image. + * - 'initial': it will adapt the space between characters to fill + * the image with the text. The text will be the first letter of + * every word in the name, separated by dots. + */ + init: function (parent, options) { + this._super.apply(this, arguments); + options = options || {}; + this.htmlId = _.uniqueId(); + this.defaultName = options.defaultName || ''; + this.signatureRatio = options.signatureRatio || 3.0; + this.signatureType = options.signatureType || 'signature'; + this.signMode = options.mode || 'draw'; + this.currentFont = 0; + this.drawTimeout = null; + this.drawPreviewTimeout = null; + }, + /** + * Loads the fonts. + * + * @override + */ + willStart: function () { + var self = this; + return $.when( + this._super.apply(this, arguments), + this._rpc({route: '/portal/sign/get_fonts'}).then(function (data) { + self.fonts = data; + }) + ); + }, + /** + * Finds the DOM elements, initializes the signature area, + * and focus the name field. + * + * @override + */ + start: function () { + var self = this; + // signature and name input + this.$signatureGroup = this.$('.o_portal_sign_signature_group'); + this.$signatureField = this.$('.o_portal_sign_signature'); + this.$nameInput = this.$('.o_portal_sign_name_input'); + + // mode selection buttons + this.$drawButton = this.$('a.o_portal_sign_draw_button'); + this.$autoButton = this.$('a.o_portal_sign_auto_button'); + this.$loadButton = this.$('a.o_portal_sign_load_button'); + + // mode: draw + this.$drawClear = this.$('.o_portal_sign_draw_clear'); + + // mode: auto + this.$autoSelectStyle = this.$('.o_portal_sign_auto_select_style'); + this.$autoFontSelection = this.$('.o_portal_sign_auto_font_selection'); + this.$autoFontList = this.$('.o_portal_sign_auto_font_list'); + for (var i in this.fonts) { + var $img = $('').addClass('img-fluid'); + var $a = $('').addClass('btn p-0').append($img).data('fontNb', i); + this.$autoFontList.append($a); + } + + // mode: load + this.$loadFile = this.$('.o_portal_sign_load_file'); + this.$loadInvalid = this.$('.o_portal_sign_load_invalid'); + + // Resize the signature area if it is resized + $(window).on('resize.o_portal_sign_name_and_signature', _.debounce(function () { + if (self.isDestroyed()) { + // May happen since this is debounced + return; + } + self.resetSignature(); + }, 250)); + + return this._super.apply(this, arguments).then(function () { + // initialize the signature area + return self.resetSignature(); + }); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + $(window).off('resize.o_portal_sign_name_and_signature'); + }, + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Focuses the name. + */ + focusName: function () { + this.$nameInput.focus(); + }, + + /** + * Gets the name currently given by the user. + * + * @returns {string} name + */ + getName: function () { + return this.$nameInput.val(); + }, + /** + * Gets the signature currently drawn. The data format is that produced + * natively by Canvas - base64 encoded (likely PNG) bitmap data. + * + * @returns {string[]} Array that contains the signature as a bitmap. + * The first element is the mimetype, the second element is the data. + */ + getSignatureImage: function () { + return this.$signatureField.jSignature('getData', 'image'); + }, + /** + * Gets the signature currently drawn, in a format ready to be used in + * an src attribute. + * + * @returns {string} the signature currently drawn, src ready + */ + getSignatureImageSrc: function () { + return this.$signatureField.jSignature('getData'); + }, + /** + * Returns whether the drawing area is currently empty. + * + * @returns {boolean} Whether the drawing area is currently empty. + */ + isSignatureEmpty: function () { + var signature = this.$signatureField.jSignature('getData'); + return signature ? this.emptySignature === signature : true; + }, + /** + * (Re)initializes the signature area: + * - set the correct width and height of the drawing based on the width + * of the container and the ratio option + * - empty any previous content + * - correctly reset the empty state + * - call @see setMode with reset + * + * @returns {Deferred} + */ + resetSignature: function () { + if (!this.$signatureField) { + // no action if called before start + return $.Deferred().reject(); + } + // recompute size based on the current width + this.$signatureField.css({width: 'unset'}); + var width = this.$signatureField.width(); + var height = parseInt(width / this.signatureRatio); + + // necessary because the lib is adding invisible div with margin + // signature field too tall without this code + this.$signatureField.css({ + width: width, + height: height, + }); + + this.$signatureField + .empty() + .jSignature({ + 'decor-color': '#D1D0CE', + 'background-color': '#FFF', + 'color': '#000', + 'lineWidth': 2, + 'width': width, + 'height': height, + }); + + this.emptySignature = this.$signatureField.jSignature('getData'); + + this.setMode(this.signMode, true); + + this.focusName(); + + return $.Deferred().resolve(); + }, + /** + * Changes the signature mode. Toggles the display of the relevant + * controls and resets the drawing. + * + * @param {string} mode - the mode to use. Can be one of the following: + * - 'draw': the user draws the signature manually with the mouse + * - 'auto': the signature is drawn automatically using a selected font + * - 'load': the signature is loaded from an image file + * @param {boolean} [reset=false] - Set to true to reset the elements + * even if the @see mode has not changed. By default nothing happens + * if the @see mode is already selected. + */ + setMode: function (mode, reset) { + if (reset !== true && mode === this.signMode) { + // prevent flickering and unnecessary compute + return; + } + + this.signMode = mode; + + this.$drawClear.toggleClass('d-none', this.signMode !== 'draw'); + this.$autoSelectStyle.toggleClass('d-none', this.signMode !== 'auto'); + this.$loadFile.toggleClass('d-none', this.signMode !== 'load'); + + this.$drawButton.toggleClass('active', this.signMode === 'draw'); + this.$autoButton.toggleClass('active', this.signMode === 'auto'); + this.$loadButton.toggleClass('active', this.signMode === 'load'); + + this.$signatureField.jSignature(this.signMode === 'draw' ? 'enable' : 'disable'); + this.$signatureField.jSignature('reset'); + + if (this.signMode === 'auto') { + // draw based on name + this._drawCurrentName(); + } else { + // close style dialog + this.$autoFontSelection.addClass('d-none'); + } + + if (this.signMode !== 'load') { + // close invalid file alert + this.$loadInvalid.addClass('d-none'); + } + }, + /** + * Gets the current name and signature, validates them, and returns + * the result. If they are invalid, displays the errors to the user. + * + * @returns {boolean} whether the current name and signature are valid + */ + validateSignature: function () { + var name = this.getName(); + var isSignatureEmpty = this.isSignatureEmpty(); + this.$nameInput.parent().toggleClass('o_has_error', !name) + .find('.form-control, .custom-select').toggleClass('is-invalid', !name); + this.$signatureGroup.toggleClass('border-danger', isSignatureEmpty); + return name && !isSignatureEmpty; + }, + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * Draws the current name with the current font in the signature field. + * + * @private + */ + _drawCurrentName: function () { + var font = this.fonts[this.currentFont]; + var text = this._getCleanedName(); + var canvas = this.$signatureField.find('canvas')[0]; + var img = this._getSVGText(font, text, canvas.width, canvas.height); + return this._printImage(img); + }, + /** + * Returns the given name after cleaning it by removing characters that + * are not supposed to be used in a signature. If @see signatureType is set + * to 'initial', returns the first letter of each word, separated by dots. + * + * @private + * @returns {string} cleaned name + */ + _getCleanedName: function () { + var text = this.getName().replace(/[^[\w\u00E0-\u00FC]-'" ]/g, ''); + if (this.signatureType === 'initial') { + return (text.split(' ').map(function (w) { + return w[0]; + }).join('.') + '.'); + } + return text; + }, + /** + * Gets an SVG matching the given parameters, output compatible with the + * src attribute of . + * + * @private + * @param {string} font: base64 encoded font to use + * @param {string} text: the name to draw + * @param {number} width: the width of the resulting image in px + * @param {number} height: the height of the resulting image in px + * @returns {string} image = mimetype + image data + */ + _getSVGText: function (font, text, width, height) { + var $svg = $(core.qweb.render('portal.sign_svg_text', { + width: width, + height: height, + font: font, + text: text, + type: this.signatureType, + })); + $svg.attr({ + 'xmlns': "http://www.w3.org/2000/svg", + 'xmlns:xlink': "http://www.w3.org/1999/xlink", + }); + + return "data:image/svg+xml," + encodeURI($svg[0].outerHTML); + }, + /** + * Displays the given image in the signature field. + * If needed, resizes the image to fit the existing area. + * + * @private + * @param {string} imgSrc - data of the image to display + */ + _printImage: function (imgSrc) { + var self = this; + + var image = new Image; + image.onload = function () { + // don't slow down the UI if the drawing is slow, and prevent + // drawing twice when calling this method in rapid succession + clearTimeout(self.drawTimeout); + self.drawTimeout = setTimeout(function () { + var width = 0; + var height = 0; + var ratio = image.width / image.height; + + var $canvas = self.$signatureField.find('canvas'); + var context = $canvas[0].getContext('2d'); + + if (image.width / $canvas[0].width > image.height / $canvas[0].height) { + width = $canvas[0].width; + height = parseInt(width / ratio); + } else { + height = $canvas[0].height; + width = parseInt(height * ratio); + } + self.$signatureField.jSignature('reset'); + var ignoredContext = _.pick(context, ['shadowOffsetX', 'shadowOffsetY']); + _.extend(context, {shadowOffsetX: 0, shadowOffsetY: 0}); + context.drawImage(image, + 0, + 0, + image.width, + image.height, + ($canvas[0].width - width) / 2, + ($canvas[0].height - height) / 2, + width, + height + ); + _.extend(context, ignoredContext); + }, 0); + }; + image.src = imgSrc; + }, + /** + * Sets the font to use in @see mode 'auto'. Redraws the signature if + * the font has been changed. + * + * @private + * @param {number} index - index of the font in @see this.fonts + */ + _setFont: function (index) { + if (index !== this.currentFont) { + this.currentFont = index; + this._drawCurrentName(); + } + }, + /** + * Updates the preview buttons by rendering the signature for each font. + * + * @private + */ + _updatePreviewButtons: function () { + var self = this; + // don't slow down the UI if the drawing is slow, and prevent + // drawing twice when calling this method in rapid succession + clearTimeout(this.drawPreviewTimeout); + this.drawPreviewTimeout = setTimeout(function () { + var height = 100; + var width = parseInt(height * self.signatureRatio); + var $existingButtons = self.$autoFontList.find('a'); + for (var i = 0; i < self.fonts.length; i++) { + var imgSrc = self._getSVGText( + self.fonts[i], + self._getCleanedName() || _t("Your name"), + width, + height + ); + $existingButtons.eq(i).find('img').attr('src', imgSrc); + } + }, 0); + }, + + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- + + /** + * Handles click on the signature: closes the font selection. + * + * @see mode 'auto' + * @private + * @param {Event} ev + */ + _onClickSignature: function (ev) { + this.$autoFontSelection.addClass('d-none'); + }, + /** + * Handles click on the Auto button: activates @see mode 'auto'. + * + * @private + * @param {Event} ev + */ + _onClickSignAutoButton: function (ev) { + ev.preventDefault(); + this.setMode('auto'); + }, + /** + * Handles click on a font: uses it and closes the font selection. + * + * @see mode 'auto' + * @private + * @param {Event} ev + */ + _onClickSignAutoFontSelection: function (ev) { + this.$autoFontSelection.addClass('d-none').removeClass('d-flex').css('width', 0); + this._setFont(parseInt($(ev.currentTarget).data('font-nb'))); + }, + /** + * Handles click on Select Style: opens and updates the font selection. + * + * @see mode 'auto' + * @private + * @param {Event} ev + */ + _onClickSignAutoSelectStyle: function (ev) { + var self = this; + var width = Math.min( + self.$autoFontSelection.find('a').first().height() * self.signatureRatio * 1.25, + this.$signatureField.width() + ); + + ev.preventDefault(); + self._updatePreviewButtons(); + + this.$autoFontSelection.removeClass('d-none').addClass('d-flex'); + this.$autoFontSelection.show().animate({'width': width}, 500, function () {}); + }, + /** + * Handles click on the Draw button: activates @see mode 'draw'. + * + * @private + * @param {Event} ev + */ + _onClickSignDrawButton: function (ev) { + ev.preventDefault(); + this.setMode('draw'); + }, + /** + * Handles click on clear: empties the signature field. + * + * @see mode 'draw' + * @private + * @param {Event} ev + */ + _onClickSignDrawClear: function (ev) { + ev.preventDefault(); + this.$signatureField.jSignature('reset'); + }, + /** + * Handles click on the Load button: activates @see mode 'load'. + * + * @private + * @param {Event} ev + */ + _onClickSignLoadButton: function (ev) { + ev.preventDefault(); + // open file upload automatically (saves 1 click) + this.$loadFile.find('input').click(); + this.setMode('load'); + }, + /** + * Handles change on load file input: displays the loaded image if the + * format is correct, or diplays an error otherwise. + * + * @see mode 'load' + * @private + * @param {Event} ev + */ + _onChangeSignLoadInput: function (ev) { + var f = ev.target.files[0]; + if (f.type.substr(0, 5) !== 'image') { + this.$signatureField.jSignature('reset'); + this.$loadInvalid.removeClass('d-none'); + return false; + } + this.$loadInvalid.addClass('d-none'); + + var self = this; + var reader = new FileReader(); + reader.onload = function (ev) { + self._printImage(this.result); + }; + reader.readAsDataURL(f); + }, + /** + * Handles input on name field: if the @see mode is 'auto', redraws the + * signature with the new name. Also updates the font selection if open. + * + * @private + * @param {Event} ev + */ + _onInputSignName: function (ev) { + if (this.signMode !== 'auto') { + return; + } + this._drawCurrentName(); + if (!this.$autoFontSelection.hasClass('d-none')) { + this._updatePreviewButtons(); + } + }, + /** + * Handles mouse over on font selection: uses this font. + * + * @see mode 'auto' + * @private + * @param {Event} ev + */ + _onMouseOverSignAutoFontSelection: function (ev) { + this._setFont(parseInt($(ev.currentTarget).data('font-nb'))); + }, + /** + * Handles touch start on font selection: uses this font. + * + * @see mode 'auto' + * @private + * @param {Event} ev + */ + _onTouchStartSignAutoFontSelection: function (ev) { + this._setFont(parseInt($(ev.currentTarget).data('font-nb'))); + }, +}); + +return { + NameAndSignature: NameAndSignature, +}; +}); diff --git a/addons/portal/static/src/js/portal_signature.js b/addons/portal/static/src/js/portal_signature.js index 825db234cd3..f2741c7d741 100644 --- a/addons/portal/static/src/js/portal_signature.js +++ b/addons/portal/static/src/js/portal_signature.js @@ -1,93 +1,127 @@ -odoo.define('portal.signature_form', function (require){ - "use strict"; +odoo.define('portal.signature_form', function (require) { + 'use strict'; - var ajax = require('web.ajax'); - var base = require('web_editor.base'); var core = require('web.core'); - var Widget = require("web.Widget"); - var rpc = require("web.rpc"); - + var dom = require('web.dom'); + var NameAndSignature = require('portal.name_and_signature').NameAndSignature; var qweb = core.qweb; + var Widget = require('web.Widget'); + 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 = Widget.extend({ template: 'portal.portal_signature', + xmlDependencies: ['/portal/static/src/xml/portal_signature.xml'], events: { - 'click #o_portal_sign_clear': 'clearSign', - 'click .o_portal_sign_submit': 'submitSign', - 'init #o_portal_sign_accept': 'initSign', + 'click .o_portal_sign_submit': '_onClickSignSubmit', }, - init: function(parent, options) { + /** + * 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.options = _.extend(options || {}, { - csrf_token: odoo.csrf_token, - }); + + 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 || {}); + + // debounce on click to not make multiple RPC if clicking too fast + this._onClickSignSubmit = dom.makeButtonHandler(this._onClickSignSubmit); + }, + /** + * Overridden to get the DOM elements + * and to insert the name and signature. + * + * @override + */ + start: function () { + this.$confirm_btn = this.$('.o_portal_sign_submit'); + this.$controls = this.$('.o_portal_sign_controls'); + this.nameAndSignature.replace(this.$('.o_portal_sign_name_and_signature')); + return this._super.apply(this, arguments); }, - willStart: function() { - return this._loadTemplates(); + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Focuses the name. + * + * @see NameAndSignature.focusName(); + */ + focusName: function () { + this.nameAndSignature.focusName(); + }, + /** + * Resets the signature. + * + * @see NameAndSignature.resetSignature(); + */ + resetSignature: function () { + return this.nameAndSignature.resetSignature(); }, - start: function() { - this.initSign(); - }, + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- - // Signature - initSign: function () { - this.$("#o_portal_signature").empty().jSignature({ - 'decor-color': '#D1D0CE', - 'color': '#000', - 'background-color': '#fff', - 'height': '142px', - 'width': '100%', // prevent the signature from being too big - }); - this.empty_sign = this.$("#o_portal_signature").jSignature('getData', 'image'); - }, - - clearSign: function () { - this.$("#o_portal_signature").jSignature('reset'); - }, - - submitSign: function (ev) { + /** + * 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(); - // extract data - var self = this; - var $confirm_btn = self.$el.find('button[type="submit"]'); - - // process : display errors, or submit - var partner_name = self.$("#o_portal_sign_name").val(); - var signature = self.$("#o_portal_signature").jSignature('getData', 'image'); - var is_empty = signature ? this.empty_sign[1] === signature[1] : true; - - this.$('#o_portal_sign_name').parent().toggleClass('o_has_error', !partner_name).find('.form-control, .custom-select').toggleClass('is-invalid', !partner_name); - this.$('#o_portal_sign_draw').toggleClass('bg-danger text-white', is_empty); - if (is_empty || ! partner_name) { - return false; + if (!this.nameAndSignature.validateSignature()) { + return; } - $confirm_btn.prepend(' '); - $confirm_btn.attr('disabled', true); + var name = this.nameAndSignature.getName(); + var signature = this.nameAndSignature.getSignatureImage()[1]; - return rpc.query({ - route: this.options.callUrl, - params: { - 'res_id': this.options.resId, - 'access_token': this.options.accessToken, - 'partner_name': partner_name, - 'signature': signature ? signature[1] : false, - }, + return this._rpc({ + route: this.callUrl, + params: _.extend(this.rpcParams, { + 'name': name, + 'signature': signature, + }), }).then(function (data) { - self.$('.fa-spinner').remove(); if (data.error) { self.$('.o_portal_sign_error_msg').remove(); - $confirm_btn.before(qweb.render('portal.portal_signature_error', {message: data.error})); - $confirm_btn.attr('disabled', false); - } - else if (data.success) { - $confirm_btn.remove(); - var $success = qweb.render("portal.portal_signature_success", {widget: data}); - self.$('#o_portal_sign_draw').parent().replaceWith($success); + 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) { @@ -95,37 +129,55 @@ odoo.define('portal.signature_form', function (require){ } else { window.location.reload(); } + // no resolve if we reload the page + return $.Deferred(); } }); }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @returns {Deferred} - */ - _loadTemplates: function () { - return ajax.loadXML('/portal/static/src/xml/portal_signature.xml', qweb); - }, - }); - - base.ready().then(function () { - $('.o_portal_signature_form').each(function () { - var $elem = $(this); - var form = new SignatureForm(null, $elem.data()); - form.appendTo($elem); - }); - // Make the signature responsive when it is displayed in bootstrap modal. - // More precisely it is too small if this code is not here. - $('.o_portal_signature_form').parents('.modal').on('shown.bs.modal', function (ev) { - $('.o_portal_signature_form').trigger('resize'); - }); }); return { SignatureForm: SignatureForm, }; }); + +odoo.define('portal.signature_form_init', function (require) { +'use strict'; + +var SignatureForm = require('portal.signature_form').SignatureForm; +var rootWidget = require('root.widget'); +require('web.dom_ready'); + +$('.o_portal_signature_form').each(function () { + var hasBeenReset = false; + var $elem = $(this); + + var callUrl = $elem.data('call-url'); + var nameAndSignatureOptions = { + defaultName: $elem.data('default-name'), + mode: $elem.data('mode'), + signatureRatio: $elem.data('signature-ratio'), + signatureType: $elem.data('signature-type'), + }; + var sendLabel = $elem.data('send-label'); + + var form = new SignatureForm(rootWidget, { + callUrl: callUrl, + nameAndSignatureOptions: nameAndSignatureOptions, + sendLabel: sendLabel, + }); + form.appendTo($elem); + // Correctly set up the signature area if it is inside a modal + $('.o_portal_signature_form').parents('.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(); + } + }); +}); + +}); diff --git a/addons/portal/static/src/scss/portal.scss b/addons/portal/static/src/scss/portal.scss index 190a9c23fa9..73c1c20d421 100644 --- a/addons/portal/static/src/scss/portal.scss +++ b/addons/portal/static/src/scss/portal.scss @@ -432,10 +432,6 @@ img.o_we_custom_image { } } -.o_portal_signature_form .o_portal_sign_error_msg { - color: $danger; -} - form label { font-weight: $font-weight-bold; &.label-optional { diff --git a/addons/portal/static/src/scss/portal_name_and_signature.scss b/addons/portal/static/src/scss/portal_name_and_signature.scss new file mode 100644 index 00000000000..557731948fb --- /dev/null +++ b/addons/portal/static/src/scss/portal_name_and_signature.scss @@ -0,0 +1,23 @@ +.o_portal_sign_name_and_signature { + // for the absolute of the font selection to work + position: relative; +} + +/* Signature Dialog */ +// The .card selector is important to make sure the position absolute will take +// priority over the position relative of the card. +.card.o_portal_sign_auto_font_selection { + @include o-position-absolute(0, 0); + + .o_portal_sign_auto_font_list { + overflow: auto; + + > a { + height: 100px; + + > img { + height: 100%; + } + } + } +} diff --git a/addons/portal/static/src/xml/portal_name_and_signature.xml b/addons/portal/static/src/xml/portal_name_and_signature.xml new file mode 100644 index 00000000000..a8c111710e6 --- /dev/null +++ b/addons/portal/static/src/xml/portal_name_and_signature.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+
Styles
+
+
+
+ + diff --git a/addons/portal/static/src/xml/portal_signature.xml b/addons/portal/static/src/xml/portal_signature.xml index 2b2fcd148b0..9904ce88407 100644 --- a/addons/portal/static/src/xml/portal_signature.xml +++ b/addons/portal/static/src/xml/portal_signature.xml @@ -1,35 +1,35 @@ + + -
- -
- - -
-
-
-
- Clear -
- Draw your signature + + +
+
+
+
-
-
-
-
+ + -
- +
+
diff --git a/addons/portal/views/assets.xml b/addons/portal/views/assets.xml index f9a9197f855..ee955dfec47 100644 --- a/addons/portal/views/assets.xml +++ b/addons/portal/views/assets.xml @@ -27,8 +27,12 @@ diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 0f44fb75fdb..77cb9578362 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -481,22 +481,33 @@ + Snippet to request user signature in the portal. The feature comes with + the JS file `portal_signature.js`. -