summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/widgets/name_and_signature.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/widgets/name_and_signature.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/widgets/name_and_signature.js')
-rw-r--r--addons/web/static/src/js/widgets/name_and_signature.js662
1 files changed, 662 insertions, 0 deletions
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 00000000..abc58a87
--- /dev/null
+++ b/addons/web/static/src/js/widgets/name_and_signature.js
@@ -0,0 +1,662 @@
+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 = $('<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.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 <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;
+ },
+ 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 <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,
+ 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,
+};
+});