odoo.define('web.name_and_signature', function (require) { 'use strict'; var core = require('web.core'); var config = require('web.config'); var utils = require('web.utils'); 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: 'web.sign_name_and_signature', xmlDependencies: ['/web/static/src/xml/name_and_signature.xml'], events: { // name 'input .o_web_sign_name_input': '_onInputSignName', // signature 'click .o_web_sign_signature': '_onClickSignature', 'change .o_web_sign_signature': '_onChangeSignature', // draw 'click .o_web_sign_draw_button': '_onClickSignDrawButton', 'click .o_web_sign_draw_clear a': '_onClickSignDrawClear', // auto 'click .o_web_sign_auto_button': '_onClickSignAutoButton', 'click .o_web_sign_auto_select_style a': '_onClickSignAutoSelectStyle', 'click .o_web_sign_auto_font_selection a': '_onClickSignAutoFontSelection', 'mouseover .o_web_sign_auto_font_selection a': '_onMouseOverSignAutoFontSelection', 'touchmove .o_web_sign_auto_font_selection a': '_onTouchStartSignAutoFontSelection', // load 'click .o_web_sign_load_button': '_onClickSignLoadButton', 'change .o_web_sign_load_file input': '_onChangeSignLoadInput', }, /** * Allows options. * * @constructor * @param {Widget} parent * @param {Object} [options={}] * @param {number} [options.displaySignatureRatio=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.defaultFont=''] - The unique and default * font for auto mode. If empty, all fonts are visible. * * @param {string} [options.fontColor='DarkBlue'] - Color of signature * (must be a string color) * @param {string} [options.noInputName=false] - If set to true, * the user can not enter his name. If there aren't defaultName, * auto mode is hidden. * @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.defaultFont = options.defaultFont || ''; this.fontColor = options.fontColor || 'DarkBlue'; this.displaySignatureRatio = options.displaySignatureRatio || 3.0; this.signatureType = options.signatureType || 'signature'; this.signMode = options.mode || 'draw'; this.noInputName = options.noInputName || false; this.currentFont = 0; this.drawTimeout = null; this.drawPreviewTimeout = null; }, /** * Loads the fonts. * * @override */ willStart: function () { var self = this; return Promise.all([ this._super.apply(this, arguments), this._rpc({route: '/web/sign/get_fonts/' + self.defaultFont}).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_web_sign_signature_group'); this.$signatureField = this.$('.o_web_sign_signature'); this.$nameInput = this.$('.o_web_sign_name_input'); this.$nameInputGroup = this.$('.o_web_sign_name_group'); // mode selection buttons this.$drawButton = this.$('a.o_web_sign_draw_button'); this.$autoButton = this.$('a.o_web_sign_auto_button'); this.$loadButton = this.$('a.o_web_sign_load_button'); // mode: draw this.$drawClear = this.$('.o_web_sign_draw_clear'); // mode: auto this.$autoSelectStyle = this.$('.o_web_sign_auto_select_style'); this.$autoFontSelection = this.$('.o_web_sign_auto_font_selection'); this.$autoFontList = this.$('.o_web_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_web_sign_load_file'); this.$loadInvalid = this.$('.o_web_sign_load_invalid'); if (this.fonts && this.fonts.length < 2) { this.$autoSelectStyle.hide(); } if (this.noInputName) { if (this.defaultName === "") { this.$autoButton.hide(); } this.$nameInputGroup.hide(); } // Resize the signature area if it is resized $(window).on('resize.o_web_sign_name_and_signature', _.debounce(function () { if (self.isDestroyed()) { // May happen since this is debounced return; } self.resizeSignature(); }, 250)); return this._super.apply(this, arguments); }, /** * @override */ destroy: function () { this._super.apply(this, arguments); $(window).off('resize.o_web_sign_name_and_signature'); }, //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- /** * Focuses the name. */ focusName: function () { // Don't focus on mobile if (!config.device.isMobile) { 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 ? this.emptySignature === signature : true; }, resizeSignature: function() { if (!this.$signatureField) { return; } // recompute size based on the current width this.$signatureField.css({width: 'unset'}); const width = this.$signatureField.width(); const height = parseInt(width / this.displaySignatureRatio); // 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.find('canvas').css({ width: width, height: height, }); return {width, height}; }, /** * (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 Promise.reject(); } const {width, height} = this.resizeSignature(); this.$signatureField .empty() .jSignature({ 'decor-color': '#D1D0CE', 'background-color': 'rgba(255,255,255,0)', 'show-stroke': false, 'color': this.fontColor, 'lineWidth': 2, 'width': width, 'height': height, }); this.emptySignature = this.$signatureField.jSignature('getData'); this.setMode(this.signMode, true); this.focusName(); return Promise.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(); 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('web.sign_svg_text', { width: width, height: height, font: font, text: text, type: this.signatureType, color: this.fontColor, })); $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); self.trigger_up('signature_changed'); }, 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.displaySignatureRatio); 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); }, /** * Waits for the signature to be not empty and triggers up the event * `signature_changed`. * This is necessary because some methods of jSignature are async but * they don't return a promise and don't trigger any event. * * @private * @param {Deferred} [def=Deferred] - Deferred that will be returned by * the method and resolved when the signature is not empty anymore. * @returns {Deferred} */ _waitForSignatureNotEmpty: function (def) { def = def || $.Deferred(); if (!this.isSignatureEmpty()) { this.trigger_up('signature_changed'); def.resolve(); } else { // Use the existing def to prevent the method from creating a new // one at every loop. setTimeout(this._waitForSignatureNotEmpty.bind(this, def), 10); } return def; }, //---------------------------------------------------------------------- // 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.displaySignatureRatio * 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'); }, /** * Triggers up the signature change event. * * @private * @param {Event} ev */ _onChangeSignature: function (ev) { this.trigger_up('signature_changed'); }, /** * 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 * @return bool|undefined */ _onChangeSignLoadInput: function (ev) { var self = this; var f = ev.target.files[0]; if (f === undefined) { return false; } if (f.type.substr(0, 5) !== 'image') { this.$signatureField.jSignature('reset'); this.$loadInvalid.removeClass('d-none'); return false; } this.$loadInvalid.addClass('d-none'); utils.getDataURLFromFile(f).then(function (result) { self._printImage(result); }); }, /** * 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, }; });