summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorjbm-odoo <jbm@odoo.com>2019-01-14 14:22:03 +0000
committerjbm-odoo <jbm@odoo.com>2019-02-14 12:59:10 +0000
commit4960453db91b465c9429453ef3a0692a604c8ef9 (patch)
tree3b02bb92556b523e91a949817e31da970b6f2a98 /addons/web/static/src
parent7410de111fe33731ba79f68f53e13dd3d511fb43 (diff)
[IMP] web Add widget signature on binary field
Add a new widget for binary fields. It open a dialog box and the user can sign manually, or an signature can be draw automatically or he can upload a picture of his signature. Move fonts, controller, scss,templates about signature from portal to web to avoid redundance closes odoo/odoo#30222
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/fonts/sign/Aspire-DemiBold.ttfbin0 -> 30040 bytes
-rw-r--r--addons/web/static/src/fonts/sign/JennaSue.ttfbin0 -> 42948 bytes
-rw-r--r--addons/web/static/src/fonts/sign/Otto.ttfbin0 -> 96416 bytes
-rw-r--r--addons/web/static/src/fonts/sign/Pen_Frenzy.ttfbin0 -> 34156 bytes
-rw-r--r--addons/web/static/src/fonts/sign/Prophecy_Script.ttfbin0 -> 69604 bytes
-rw-r--r--addons/web/static/src/fonts/sign/Rhesmanisa.ttfbin0 -> 40532 bytes
-rw-r--r--addons/web/static/src/fonts/sign/khand.ttfbin0 -> 38344 bytes
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js1
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/signature.js272
-rw-r--r--addons/web/static/src/js/widgets/name_and_signature.js648
-rw-r--r--addons/web/static/src/scss/name_and_signature.scss49
-rw-r--r--addons/web/static/src/xml/base.xml44
-rw-r--r--addons/web/static/src/xml/name_and_signature.xml106
14 files changed, 1121 insertions, 0 deletions
diff --git a/addons/web/static/src/fonts/sign/Aspire-DemiBold.ttf b/addons/web/static/src/fonts/sign/Aspire-DemiBold.ttf
new file mode 100644
index 00000000000..d8cb1992b86
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/Aspire-DemiBold.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/JennaSue.ttf b/addons/web/static/src/fonts/sign/JennaSue.ttf
new file mode 100644
index 00000000000..667a55112dd
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/JennaSue.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/Otto.ttf b/addons/web/static/src/fonts/sign/Otto.ttf
new file mode 100644
index 00000000000..9a2a51dadd2
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/Otto.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/Pen_Frenzy.ttf b/addons/web/static/src/fonts/sign/Pen_Frenzy.ttf
new file mode 100644
index 00000000000..d9f72c59340
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/Pen_Frenzy.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/Prophecy_Script.ttf b/addons/web/static/src/fonts/sign/Prophecy_Script.ttf
new file mode 100644
index 00000000000..57fd1b3647a
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/Prophecy_Script.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/Rhesmanisa.ttf b/addons/web/static/src/fonts/sign/Rhesmanisa.ttf
new file mode 100644
index 00000000000..c0ced9d5667
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/Rhesmanisa.ttf
Binary files differ
diff --git a/addons/web/static/src/fonts/sign/khand.ttf b/addons/web/static/src/fonts/sign/khand.ttf
new file mode 100644
index 00000000000..f71b7493a3f
--- /dev/null
+++ b/addons/web/static/src/fonts/sign/khand.ttf
Binary files differ
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index ae080f45a11..dcfdc982b7d 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -3005,6 +3005,7 @@ return {
FieldEmail: FieldEmail,
FieldBinaryFile: FieldBinaryFile,
FieldPdfViewer: FieldPdfViewer,
+ AbstractFieldBinary: AbstractFieldBinary,
FieldBinaryImage: FieldBinaryImage,
FieldBoolean: FieldBoolean,
FieldBooleanButton: FieldBooleanButton,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 35d10c5a5e4..a8bd80719e1 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -37,6 +37,7 @@ registry
.add('CopyClipboardText', basic_fields.TextCopyClipboard)
.add('CopyClipboardChar', basic_fields.CharCopyClipboard)
.add('image', basic_fields.FieldBinaryImage)
+ .add('signature', basic_fields.FieldBinarySignature)
.add('binary', basic_fields.FieldBinaryFile)
.add('pdf_viewer', basic_fields.FieldPdfViewer)
.add('monetary', basic_fields.FieldMonetary)
diff --git a/addons/web/static/src/js/fields/signature.js b/addons/web/static/src/js/fields/signature.js
new file mode 100644
index 00000000000..186c8fd764c
--- /dev/null
+++ b/addons/web/static/src/js/fields/signature.js
@@ -0,0 +1,272 @@
+odoo.define('web.Signature', function (require) {
+ "use strict";
+
+ var AbstractFieldBinary = require('web.basic_fields').AbstractFieldBinary;
+ var core = require('web.core');
+ var Dialog = require('web.Dialog');
+ var field_utils = require('web.field_utils');
+ var NameAndSignature = require('web.name_and_signature').NameAndSignature;
+ var registry = require('web.field_registry');
+ var session = require('web.session');
+ var utils = require('web.utils');
+
+
+ var qweb = core.qweb;
+ var _t = core._t;
+
+// The goal of this dialog is to ask the user a signature request.
+// It uses @see SignNameAndSignature for the name and signature fields.
+var SignatureDialog = Dialog.extend({
+ template: 'web.signature_dialog',
+ xmlDependencies: Dialog.prototype.xmlDependencies.concat(
+ ['/web/static/src/xml/name_and_signature.xml']
+ ),
+ custom_events: {
+ 'signature_changed': '_onChangeSignature',
+ },
+
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {Object} options
+ * @param {string} [options.title='Adopt Your Signature'] - modal title
+ * @param {string} [options.size='medium'] - modal size
+ * @param {Object} [options.nameAndSignatureOptions={}] - options for
+ * @see NameAndSignature.init()
+ */
+ init: function (parent, options) {
+ var self = this;
+ options = options || {};
+
+ options.title = options.title || _t("Adopt Your Signature");
+ options.size = options.size || 'medium';
+ options.technical = false;
+
+ if (!options.buttons) {
+ options.buttons = [];
+ options.buttons.push({text: _t("Adopt and Sign"), classes: "btn-primary", disabled: true, click: function (e) {
+ self._onConfirm();
+ }});
+ options.buttons.push({text: _t("Cancel"), close: true});
+ }
+
+ this._super(parent, options);
+
+ this.nameAndSignature = new NameAndSignature(this, options.nameAndSignatureOptions);
+ },
+ /**
+ * Inserts the name and signature.
+ *
+ * @override
+ */
+ start: function () {
+ var self = this;
+ this.$primaryButton = this.$footer.find('.btn-primary');
+
+ this.opened().then(function () {
+ self.nameAndSignature.replace(self.$('.o_web_sign_name_and_signature'));
+ });
+
+ return this._super.apply(this, arguments);
+ },
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * Returns whether the drawing area is currently empty.
+ *
+ * @see NameAndSignature.isSignatureEmpty()
+ * @returns {boolean} Whether the drawing area is currently empty.
+ */
+ isSignatureEmpty: function () {
+ return this.nameAndSignature.isSignatureEmpty();
+ },
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * Toggles the submit button depending on the signature state.
+ *
+ * @private
+ */
+ _onChangeSignature: function () {
+ var isEmpty = this.nameAndSignature.isSignatureEmpty();
+ this.$primaryButton.prop('disabled', isEmpty);
+ },
+ /**
+ * Upload the signature image when confirm.
+ *
+ * @private
+ */
+ _onConfirm: function (fct) {
+ this.trigger_up('upload_signature', {
+ name: this.nameAndSignature.getName(),
+ signatureImage: this.nameAndSignature.getSignatureImage(),
+ });
+ },
+});
+
+var FieldBinarySignature = AbstractFieldBinary.extend({
+ fieldDependencies: _.extend({}, AbstractFieldBinary.prototype.fieldDependencies, {
+ __last_update: {type: 'datetime'},
+ }),
+ custom_events: _.extend({}, AbstractFieldBinary.prototype.custom_events, {
+ upload_signature: '_onUploadSignature',
+ }),
+ events: _.extend({}, AbstractFieldBinary.prototype.events, {
+ 'click .o_signature': '_onClickSignature',
+ }),
+ template: null,
+ supportedFieldTypes: ['binary'],
+ file_type_magic_word: {
+ '/': 'jpg',
+ 'R': 'gif',
+ 'i': 'png',
+ 'P': 'svg+xml',
+ },
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * This widget must always have render even if there are no signature.
+ * In edit mode, the real value is return to manage required fields.
+ *
+ * @override
+ */
+ isSet: function () {
+ if (this.mode === 'edit') {
+ return this.value;
+ }
+ return true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Renders an empty signature or the saved signature. Both must have the same size.
+ *
+ * @override
+ * @private
+ */
+
+ _render: function () {
+ var self = this;
+ var displaySignatureRatio = 3;
+ var url;
+ var $img;
+ var width = this.nodeOptions.size ? this.nodeOptions.size[0] : this.attrs.width;
+ var height = this.nodeOptions.size ? this.nodeOptions.size[1] : this.attrs.height;
+ if (this.value) {
+ if (!utils.is_bin_size(this.value)) {
+ // Use magic-word technique for detecting image type
+ url = 'data:image/' + (this.file_type_magic_word[this.value[0]] || 'png') + ';base64,' + this.value;
+ } else {
+ url = session.url('/web/image', {
+ model: this.model,
+ id: JSON.stringify(this.res_id),
+ field: this.nodeOptions.preview_image || this.name,
+ // unique forces a reload of the image when the record has been updated
+ unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''),
+ });
+ }
+ $img = $(qweb.render("FieldBinarySignature-img", {widget: this, url: url}));
+ } else {
+ $img = $('<div class="o_signature o_signature_empty"><svg></svg><p>' + _t('SIGNATURE') + '</p></div>');
+ if (width && height) {
+ width = Math.min(width, displaySignatureRatio * height);
+ height = width / displaySignatureRatio;
+ } else if (width) {
+ height = width / displaySignatureRatio;
+ } else if (height) {
+ width = height * displaySignatureRatio;
+ }
+ }
+ if (width) {
+ $img.attr('width', width);
+ $img.css('max-width', width + 'px');
+ }
+ if (height) {
+ $img.attr('height', height);
+ $img.css('max-height', height + 'px');
+ }
+ this.$('> div').remove();
+ this.$('> img').remove();
+
+ this.$el.prepend($img);
+
+ $img.on('error', function () {
+ self._clearFile();
+ $img.attr('src', self.placeholder);
+ self.do_warn(_t("Image"), _t("Could not display the selected image."));
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * If the view is in edit mode, open dialog to sign.
+ *
+ * @private
+ */
+ _onClickSignature: function () {
+ var self = this;
+ if (this.mode === 'edit') {
+
+ var nameAndSignatureOptions = {
+ mode: 'draw',
+ displaySignatureRatio: 3,
+ signatureType: 'signature',
+ noInputName: true,
+ };
+
+ if (this.nodeOptions.full_name) {
+ var $search = $("[name='" + this.nodeOptions.full_name + "']");
+ // If full_name correspond to field in read-only mode
+ var signName = $($search).text();
+
+ // If full_name correspond to field to fill
+ if (!signName && $($search)[0]) {
+ signName = $($search)[0].value;
+ }
+ if (!signName) {
+ // If full_name is in the recordData. Last option because this value might not be the last value.
+ signName = this.recordData[this.nodeOptions.full_name] || '';
+ }
+ nameAndSignatureOptions.defaultName = (signName === '') ? undefined : signName;
+ }
+
+ nameAndSignatureOptions.defaultFont = this.nodeOptions.default_font || '';
+ this.signDialog = new SignatureDialog(self, {nameAndSignatureOptions: nameAndSignatureOptions});
+
+ this.signDialog.open();
+ }
+ },
+
+ /**
+ * Upload the signature image if valid and close the dialog.
+ *
+ * @private
+ */
+ _onUploadSignature: function (ev) {
+ var signatureImage = ev.data.signatureImage;
+ if (signatureImage !== this.signDialog.emptySignature) {
+ var data = signatureImage[1];
+ var type = signatureImage[0].split('/')[1];
+ this.on_file_uploaded(data.length, ev.data.name, type, data);
+ }
+ this.signDialog.close();
+ }
+});
+
+registry.add('signature', FieldBinarySignature);
+
+});
diff --git a/addons/web/static/src/js/widgets/name_and_signature.js b/addons/web/static/src/js/widgets/name_and_signature.js
new file mode 100644
index 00000000000..53355fc7aa4
--- /dev/null
+++ b/addons/web/static/src/js/widgets/name_and_signature.js
@@ -0,0 +1,648 @@
+odoo.define('web.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: '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.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.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 $.when(
+ 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 = $('<img/>').addClass('img-fluid');
+ var $a = $('<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.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_web_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 <img/> 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;
+ },
+ /**
+ * (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.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
+ .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 regexRemoveSpecialCharacters = /[^\w\u00E0-\u00FC-'" ]/g;
+ var text = this.getName().replace(regexRemoveSpecialCharacters, '');
+ 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 <img/>.
+ *
+ * @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,
+ }));
+ $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');
+
+ var reader = new FileReader();
+ reader.onload = function (ev) {
+ self._printImage(reader.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/web/static/src/scss/name_and_signature.scss b/addons/web/static/src/scss/name_and_signature.scss
new file mode 100644
index 00000000000..9dc32d5396b
--- /dev/null
+++ b/addons/web/static/src/scss/name_and_signature.scss
@@ -0,0 +1,49 @@
+.o_web_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_web_sign_auto_font_selection {
+ @include o-position-absolute(0, 0);
+
+ .o_web_sign_auto_font_list {
+ overflow: auto;
+
+ > a {
+ height: 100px;
+
+ > img {
+ height: 100%;
+ }
+ }
+ }
+}
+
+.o_field_widget {
+ .o_signature {
+ outline: 1px solid rgba(theme-color('secondary'), 0.3);
+ position: relative;
+ > p {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+}
+
+.o_field_invalid {
+ .o_signature {
+ outline: 3px solid $o-notification-error-bg-color;
+ cursor: pointer;
+ }
+}
+.o_form_editable {
+ .o_signature:hover {
+ outline: 3px solid $o-enterprise-primary-color;
+ cursor: pointer;
+ }
+}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index dc40c962333..563d6f979ae 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1006,6 +1006,50 @@
</div>
</t>
+<t t-name="FieldBinarySignature-img">
+ <img class="o_signature img img-fluid"
+ alt="Binary file"
+ t-att-src='url'
+ t-att-border="1"
+ t-att-name="widget.name"/>
+</t>
+
+<div t-name="FieldBinarySignature">
+ <div class="o_sign_signature_draw panel panel-default mt16 mb0">
+ <div class="card-header">
+ <a role="button" class="o_sign_select_style float-right btn btn-link">Select Signature Style</a>
+ <a role="button" class="o_sign_clean float-right btn btn-link">Clear Signature</a>
+ <a data-mode="draw" class="o_sign_mode o_sign_mode_draw btn btn-primary" role="button">Draw</a>
+ <a data-mode="auto" class="o_sign_mode o_sign_mode_auto btn" role="button">Auto</a>
+ <a data-mode="load" class="o_sign_mode o_sign_mode_load btn" role="button">Load</a>
+ <input type="file" name="files[]" class="o_sign_load btn btn-link" role="button"/>
+ </div>
+ <div class="o_sign_signature card-body"/>
+ </div>
+ <div class="mt16 small">By clicking Adopt and Sign, I agree that the chosen signature/initials will be a valid electronic representation of my hand-written signature/initials for all purposes when it is used on documents, including legally binding contracts.</div>
+
+ <div class="o_sign_font_dialog card">
+ <div class="card-header">Styles</div>
+ <div class="o_sign_font_selection card-body"/>
+ </div>
+</div>
+
+<t t-name="SignatureSvgText" name="SVG Signature Text">
+ <svg t-att-width="width" t-att-height="height">
+ <defs>
+ <style type="text/css">
+ @font-face {
+ font-family: "font_sign";
+ src: url(data:font/ttf;base64,<t t-esc="font"/>) format("woff");
+ font-weight: normal;
+ font-style: normal;
+ }
+ </style>
+ </defs>
+ <text x="50%" t-att-y="height*4/5" t-att-font-size="height*4/5" t-att-textLength="width * 4 / 5 * Math.min(1, text.length / 7)" lengthAdjust="spacingAndGlyphs" style="font-family:'font_sign'" fill="black" text-anchor="middle"><t t-esc="text"/></text>
+ </svg>
+</t>
+
<t t-name="FieldPdfViewer">
<div class="o_field_pdfviewer" aria-atomic="true">
<div t-if="widget.mode !== 'readonly'" class="o_form_pdf_controls mt8" role="toolbar" aria-label="PDF controls">
diff --git a/addons/web/static/src/xml/name_and_signature.xml b/addons/web/static/src/xml/name_and_signature.xml
new file mode 100644
index 00000000000..bf910fa14ea
--- /dev/null
+++ b/addons/web/static/src/xml/name_and_signature.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <!--
+ This template is used to generate the SVG containing the signature when
+ using mode 'auto'.
+
+ The following variables have to be defined:
+ - width: (number) the width of the resulting image in px
+ - height: (number) the height of the resulting image in px
+ - font: base64 encoded font to use
+ - text: (string) the name to draw
+ - type: (string)
+ -> if set to 'signature', it will adapt the characters width to
+ fit the whole text in the image.
+ -> otherwise, it will adapt the space between characters to fill
+ the image with the text.
+ -->
+ <t t-name="web.sign_svg_text" name="SVG Signature Text">
+ <svg t-att-width="width" t-att-height="height">
+ <defs>
+ <style type="text/css">
+ @font-face {
+ font-family: "font";
+ src: url(data:font/ttf;base64,<t t-esc="font"/>) format("woff");
+ font-weight: normal;
+ font-style: normal;
+ }
+ </style>
+ </defs>
+ <!-- adapt textLength to prevent distortion and left overflow with small words -->
+ <text t-esc="text" x="50%" t-att-y="height * 4 / 5" t-att-font-size="height * 4 / 5"
+ t-att-textLength="width * 4 / 5 * (type === 'signature' ? Math.min(1, text.length / 7) : 1)"
+ t-att-lengthAdjust="'spacing' + (type === 'signature' ? 'AndGlyphs' : '')"
+ style="font-family:'font'" fill="black" text-anchor="middle"/>
+ </svg>
+ </t>
+
+ <!-- Template for the widget webSignNameAndDraw. -->
+ <t t-name="web.sign_name_and_signature">
+ <div class="o_web_sign_name_and_signature">
+ <div class="o_web_sign_name_group form-group">
+ <label class="col-form-label" t-att-for="'o_web_sign_name_input_' + widget.htmlId">Full Name</label>
+ <input type="text" name="signer" t-att-id="'o_web_sign_name_input_' + widget.htmlId" class="o_web_sign_name_input form-control" t-att-value="widget.defaultName" placeholder="Your name" required=""/>
+ </div>
+
+ <!--
+ Overflow hidden is necessary because the card has a rounded
+ border, but we remove the padding from the body, so the canvas
+ will overflow on the corners without this rule.
+ -->
+ <div class="o_web_sign_signature_group card mt-3" style="overflow: hidden;">
+ <div class="card-header">
+ <div class="row no-gutters">
+ <div class="col-auto">
+ <a role="button" href="#" t-attf-class="o_web_sign_draw_button mr-2 btn btn-light {{ widget.signMode === 'draw' ? 'active': '' }}">
+ Draw
+ </a>
+ </div>
+ <div class="col-auto">
+ <a role="button" href="#" t-attf-class="o_web_sign_auto_button mr-2 btn btn-light {{ widget.signMode === 'auto' ? 'active': '' }}">
+ Auto
+ </a>
+ </div>
+ <div class="col-auto">
+ <a role="button" href="#" t-attf-class="o_web_sign_load_button mr-2 btn btn-light {{ widget.signMode === 'load' ? 'active': '' }}">
+ Load
+ </a>
+ </div>
+ <div t-attf-class="o_web_sign_draw_clear col-auto ml-auto {{ widget.signMode !== 'draw' ? 'd-none' : '' }}">
+ <a role="button" href="#" class="btn btn-link">
+ Clear
+ </a>
+ </div>
+ <div t-attf-class="o_web_sign_auto_select_style col-auto ml-auto {{ widget.signMode !== 'auto' ? 'd-none' : '' }}">
+ <a role="button" href="#" class="btn btn-link">
+ Style
+ </a>
+ </div>
+ <div t-attf-class="o_web_sign_load_file col-auto {{ widget.signMode !== 'load' ? 'd-none' : '' }}">
+ <input type="file" role="button" name="files[]" class="btn btn-link py-0"/>
+ </div>
+ </div>
+ </div>
+
+ <div class="o_web_sign_signature card-body p-0"/>
+
+ <div t-attf-class="o_web_sign_load_invalid card-footer d-none">
+ <div class="alert alert-danger mb-0" role="alert">
+ This file is invalid. Please select an image.
+ </div>
+ </div>
+ </div>
+
+ <div class="o_web_sign_auto_font_selection card d-none h-100 flex-column">
+ <div class="card-header">Styles</div>
+ <div class="o_web_sign_auto_font_list card-body text-center p-3 flex-grow-1"/>
+ </div>
+ </div>
+ </t>
+ <div t-name="web.signature_dialog">
+ <div class="o_web_sign_name_and_signature"/>
+
+ <div class="mt16 small">By clicking Adopt and Sign, I agree that the chosen signature/initials will be a valid electronic representation of my hand-written signature/initials for all purposes when it is used on documents, including legally binding contracts.</div>
+ </div>
+</templates>