[REF] web,sale,portal: adapt SignatureForm to owl

This commit converts the SignatureForm widget into a Component.
This allows to remove the last usage of the legacy NameAndSignature
widget, which we thus remove.

Part of task~3439226

Part-of: odoo/odoo#138054
This commit is contained in:
Aaron Bohy
2023-10-11 08:18:46 +00:00
parent 634409188c
commit 4bf1384e8b
21 changed files with 144 additions and 1163 deletions
@@ -56,7 +56,6 @@
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/legacy/js/promise_extension.js',
@@ -104,8 +103,6 @@
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
('include', 'web._assets_bootstrap'),
+1 -2
View File
@@ -43,11 +43,10 @@ a dependency towards website editing and customization capabilities.""",
'portal/static/src/xml/portal_chatter.xml',
'portal/static/src/js/portal_composer.js',
'portal/static/src/js/portal_security.js',
'portal/static/src/js/portal_signature.js',
'portal/static/src/xml/portal_signature.xml',
'portal/static/src/js/portal_sidebar.js',
'portal/static/src/xml/portal_security.xml',
'portal/static/src/js/components/**/*',
'portal/static/src/signature_form/**/*',
],
'web.assets_tests': [
'portal/static/tests/**/*',
@@ -1,192 +0,0 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { renderToElement } from "@web/core/utils/render";
import publicWidget from "@web/legacy/js/public/public_widget";
import { NameAndSignature } from "@web/legacy/js/widgets/name_and_signature";
/**
* This widget is a signature request form. It uses
* @see NameAndSignature for the input fields, adds a submit
* button, and handles the RPC to save the result.
*/
var SignatureForm = publicWidget.Widget.extend({
template: 'portal.portal_signature',
events: {
'click .o_portal_sign_submit': 'async _onClickSignSubmit',
},
custom_events: {
'signature_changed': '_onChangeSignature',
},
/**
* Overridden to allow options.
*
* @constructor
* @param {Widget} parent
* @param {Object} options
* @param {string} options.callUrl - make RPC to this url
* @param {string} [options.sendLabel='Accept & Sign'] - label of the
* send button
* @param {Object} [options.rpcParams={}] - params for the RPC
* @param {Object} [options.nameAndSignatureOptions={}] - options for
* @see NameAndSignature.init()
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.csrf_token = odoo.csrf_token;
this.callUrl = options.callUrl || '';
this.rpcParams = options.rpcParams || {};
this.sendLabel = options.sendLabel || _t("Accept & Sign");
this.nameAndSignature = new NameAndSignature(this,
options.nameAndSignatureOptions || {});
this.rpc = this.bindService("rpc");
},
/**
* Overridden to get the DOM elements
* and to insert the name and signature.
*
* @override
*/
start: function () {
var self = this;
this.$confirm_btn = this.$('.o_portal_sign_submit');
this.$controls = this.$('.o_portal_sign_controls');
var subWidgetStart = this.nameAndSignature.replace(this.$('.o_web_sign_name_and_signature'));
return Promise.all([subWidgetStart, this._super.apply(this, arguments)]).then(function () {
self.nameAndSignature.resetSignature();
});
},
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* Focuses the name.
*
* @see NameAndSignature.focusName();
*/
focusName: function () {
this.nameAndSignature.focusName();
},
/**
* Resets the signature.
*
* @see NameAndSignature.resetSignature();
*/
resetSignature: function () {
return this.nameAndSignature.resetSignature();
},
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* Handles click on the submit button.
*
* This will get the current name and signature and validate them.
* If they are valid, they are sent to the server, and the reponse is
* handled. If they are invalid, it will display the errors to the user.
*
* @private
* @param {Event} ev
* @returns {Deferred}
*/
_onClickSignSubmit: function (ev) {
var self = this;
ev.preventDefault();
if (!this.nameAndSignature.validateSignature()) {
return;
}
var name = this.nameAndSignature.getName();
var signature = this.nameAndSignature.getSignatureImage()[1];
return this.rpc(
this.callUrl,
Object.assign(this.rpcParams, {
'name': name,
'signature': signature,
})
).then(function (data) {
if (data.error) {
self.$('.o_portal_sign_error_msg').remove();
self.$controls.prepend(renderToElement('portal.portal_signature_error', {widget: data}));
} else if (data.success) {
var $success = renderToElement('portal.portal_signature_success', {widget: data});
self.$el.empty().append($success);
}
if (data.force_refresh) {
if (data.redirect_url) {
window.location = data.redirect_url;
} else {
window.location.reload();
}
// no resolve if we reload the page
return new Promise(function () { });
}
});
},
/**
* Toggles the submit button depending on the signature state.
*
* @private
*/
_onChangeSignature: function () {
var isEmpty = this.nameAndSignature.isSignatureEmpty();
this.$confirm_btn.prop('disabled', isEmpty);
},
});
publicWidget.registry.SignatureForm = publicWidget.Widget.extend({
selector: '.o_portal_signature_form',
/**
* @private
*/
start: function () {
var hasBeenReset = false;
var callUrl = this.$el.data('call-url');
var nameAndSignatureOptions = {
defaultName: this.$el.data('default-name'),
mode: this.$el.data('mode'),
displaySignatureRatio: this.$el.data('signature-ratio'),
signatureType: this.$el.data('signature-type'),
fontColor: this.$el.data('font-color') || 'black',
};
var sendLabel = this.$el.data('send-label');
var form = new SignatureForm(this, {
callUrl: callUrl,
nameAndSignatureOptions: nameAndSignatureOptions,
sendLabel: sendLabel,
});
// Correctly set up the signature area if it is inside a modal
this.$el.closest('.modal').on('shown.bs.modal', function (ev) {
if (!hasBeenReset) {
// Reset it only the first time it is open to get correct
// size. After we want to keep its content on reopen.
hasBeenReset = true;
form.resetSignature();
} else {
form.focusName();
}
});
return Promise.all([
this._super.apply(this, arguments),
form.appendTo(this.$el)
]);
},
});
export default SignatureForm;
@@ -0,0 +1,86 @@
/** @odoo-module **/
import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { redirect } from "@web/core/utils/urls";
import { NameAndSignature } from "@web/core/signature/name_and_signature";
import { useService } from "@web/core/utils/hooks";
/**
* This Component 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.
*/
class SignatureForm extends Component {
static template = "portal.SignatureForm"
static components = { NameAndSignature }
setup() {
this.rootRef = useRef("root");
this.rpc = useService("rpc");
this.csrfToken = odoo.csrf_token;
this.state = useState({
error: false,
success: false,
});
this.signature = useState({ name: this.props.defaultName });
this.nameAndSignatureProps = {
signature: this.signature,
fontColor: this.props.fontColor || "black",
};
if (this.props.signatureRatio) {
this.nameAndSignatureProps.displaySignatureRatio = this.props.signatureRatio;
}
if (this.props.signatureType) {
this.nameAndSignatureProps.signatureType = this.props.signatureType;
}
if (this.props.mode) {
this.nameAndSignatureProps.mode = this.props.mode;
}
// Correctly set up the signature area if it is inside a modal
onMounted(() => {
this.rootRef.el.closest('.modal').addEventListener('shown.bs.modal', () => {
this.signature.resetSignature();
});
});
}
get sendLabel() {
return this.props.sendLabel || _t("Accept & Sign");
}
/**
* 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.
*
* @returns {Promise}
*/
async onClickSubmit() {
const name = this.signature.name;
const signature = this.signature.getSignatureImage()[1];
const data = await this.rpc(this.props.callUrl, { name, signature });
if (data.force_refresh) {
if (data.redirect_url) {
redirect(data.redirect_url);
} else {
window.location.reload();
}
// do not resolve if we reload the page
return new Promise(() => {});
}
this.state.error = data.error || false;
this.state.success = !data.error && {
message: data.message,
redirectUrl: data.redirect_url,
redirectMessage: data.redirect_message,
};
}
}
registry.category("public_components").add("portal.signature_form", SignatureForm);
@@ -0,0 +1,35 @@
<templates id="template" xml:space="preserve">
<!-- Template for the widget SignatureForm. -->
<t t-name="portal.SignatureForm">
<div t-ref="root">
<div t-if="state.success" class="alert alert-success" role="status">
<span t-if="state.success.message" t-esc="state.success.message"/>
<span t-else="">Thank You!</span>
<a t-if="state.success.redirect_url" t-att-href="state.success.redirect_url">
<t t-if="state.success.redirect_message" t-esc="state.success.redirect_message"/>
<t t-else="">Click here to see your document.</t>
</a>
</div>
<t t-else="">
<NameAndSignature t-props="nameAndSignatureProps"/>
<form method="POST">
<input type="hidden" name="csrf_token" t-att-value="csrfToken"/>
<div class="o_web_sign_name_and_signature"/>
<div class="o_portal_sign_controls my-3">
<div t-if="state.error" class="o_portal_sign_error_msg alert alert-danger" role="status">
<t t-esc="state.error"/>
</div>
<div class="text-end my-3">
<button type="submit" class="o_portal_sign_submit btn btn-primary" t-on-click.prevent="onClickSubmit" t-att-disabled="signature.isSignatureEmpty ? 'disabled' : ''">
<i class="fa fa-check"/>
<t t-esc="sendLabel"/>
</button>
</div>
</div>
</form>
</t>
</div>
</t>
</templates>
@@ -1,35 +0,0 @@
<templates id="template" xml:space="preserve">
<!-- Template for the widget SignatureForm. -->
<t t-name="portal.portal_signature">
<form method="POST">
<input type="hidden" name="csrf_token" t-att-value="widget.csrf_token"/>
<div class="o_web_sign_name_and_signature"/>
<div class="o_portal_sign_controls my-3">
<div class="text-end my-3">
<button type="submit" class="o_portal_sign_submit btn btn-primary" disabled="disabled">
<i class="fa fa-check"/>
<t t-esc="widget.sendLabel"/>
</button>
</div>
</div>
</form>
</t>
<!-- Template when the sign rpc is successful. -->
<t t-name="portal.portal_signature_success">
<div class="alert alert-success" role="status">
<span t-if="widget.message" t-esc="widget.message"/>
<span t-else="">Thank You!</span>
<a t-if="widget.redirect_url" t-att-href="widget.redirect_url">
<t t-if="widget.redirect_message" t-esc="widget.redirect_message"/>
<t t-else="">Click here to see your document.</t>
</a>
</div>
</t>
<!-- Template when the sign rpc returns an error. -->
<t t-name="portal.portal_signature_error">
<div class="o_portal_sign_error_msg alert alert-danger" role="status">
<t t-esc="widget.error"/>
</div>
</t>
</templates>
+11 -11
View File
@@ -738,19 +738,19 @@
- {number} signature_ratio: ratio of the signature area
- {string} signature_type: 'signature' or 'initial'
For the default values and more information, see init() of the widgets
SignatureForm and NameAndSignature.
For default values and more information, see components SignatureForm and NameAndSignature.
-->
<template id="portal.signature_form" name="Ask Signature">
<div class="o_portal_signature_form"
t-att-data-call-url="call_url"
t-att-data-default-name="default_name"
t-att-data-mode="mode"
t-att-data-send-label="send_label"
t-att-data-signature-ratio="signature_ratio"
t-att-data-signature-type="signature_type"
t-att-data-font-color="font_color"
/>
<t t-set="signature_form_props" t-value="{
'callUrl': call_url,
'defaultName': default_name,
'mode': mode,
'sendLabel': send_label,
'signatureRatio': signature_ratio,
'signatureType': signature_type,
'fontColor': font_color
}"/>
<owl-component name="portal.signature_form" t-att-props="json.dumps(signature_form_props)"/>
</template>
<template id="portal_sidebar" name="Sidebar">
-3
View File
@@ -106,7 +106,6 @@
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/legacy/js/promise_extension.js',
@@ -154,8 +153,6 @@
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
('include', 'web._assets_bootstrap_backend'),
'base/static/src/css/modules.css',
@@ -22,11 +22,11 @@ registry.category("web_tour.tours").add('sale_signature', {
},
{
content: "click select style",
trigger: '.o_web_sign_auto_select_style a',
trigger: '.o_web_sign_auto_select_style button',
},
{
content: "click style 4",
trigger: '.o_web_sign_auto_font_selection a:eq(3)',
trigger: '.o_web_sign_auto_select_style .dropdown-item:eq(3)',
},
{
content: "click submit",
-6
View File
@@ -64,7 +64,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/legacy/js/promise_extension.js',
@@ -106,8 +105,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
'base/static/src/css/modules.css',
@@ -191,7 +188,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/legacy/scss/lazyloader.scss',
@@ -237,8 +233,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
'web/static/src/env.js',
'web/static/src/core/utils/transitions.scss', # included early because used by other files
@@ -14,6 +14,7 @@
>
<button
t-if="props.toggler !== 'parent'"
type="button"
class="dropdown-toggle"
t-attf-class="
{{props.togglerClass || ''}}
@@ -55,6 +55,7 @@ export class NameAndSignature extends Component {
this.resetSignature();
this.props.signature.getSignatureImage = () =>
this.jSignature("getData", "image");
this.props.signature.resetSignature = () => this.resetSignature();
if (this.state.signMode === "auto") {
this.drawCurrentName();
}
@@ -1,688 +0,0 @@
/** @odoo-module **/
import Widget from "@web/legacy/js/core/widget";
import { debounce } from "@web/core/utils/timing";
import { uniqueId } from "@web/core/utils/functions";
import { pick } from "@web/core/utils/objects";
import { renderToElement } from "@web/core/utils/render";
import { utils as uiUtils } from "@web/core/ui/ui_service";
import { getDataURLFromFile } from "@web/core/utils/urls";
/**
* 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.
*/
export var NameAndSignature = Widget.extend({
template: 'web.sign_name_and_signature',
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';
// default mode should be auto except when noInputName is true and no default name
this.signMode = options.mode || (options.noInputName && !this.defaultName ? 'draw' : 'auto');
this.noInputName = options.noInputName || false;
this.currentFont = 0;
this.drawTimeout = null;
this.drawPreviewTimeout = null;
this.signatureAreaHidden = false;
this.rpc = this.bindService("rpc");
},
/**
* Loads the fonts.
*
* @override
*/
willStart: function () {
/** LPR: in sign module, the web/sign/get_fonts call is cached since
* many NameAndSignature widgets are created. This way, we prevent
* doing the rpc call if it was already done inside sign
*/
if (this.fonts && this.fonts.length > 0) {
return this._super.apply(this, arguments);
}
return Promise.all([
this._super.apply(this, arguments),
this.rpc('/web/sign/get_fonts/' + this.defaultFont).then(data => {
this.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();
} else if (this.defaultName === "") {
this._hideSignatureArea();
}
// 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 (!uiUtils.isSmall()) {
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, .form-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' && text) {
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 = $(renderToElement('web.legacy.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');
Object.assign(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
);
Object.assign(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(),
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;
},
_hideSignatureArea: function () {
this.$signatureField.hide();
this.$signatureGroup.hide();
this.signatureAreaHidden = true;
},
_showSignatureArea: function () {
this.$signatureField.show();
this.$signatureGroup.show();
this.signatureAreaHidden = false;
this.resetSignature();
},
//----------------------------------------------------------------------
// 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.$signatureField.jSignature('reset'); // clears signature when clicking on draw
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');
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.signatureAreaHidden && this._getCleanedName()) {
this._showSignatureArea();
}
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')));
},
});
export default {
NameAndSignature: NameAndSignature,
};
@@ -1,23 +0,0 @@
.o_web_sign_signature_container {
// for the absolute of the frame 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%;
}
}
}
}
@@ -1,103 +0,0 @@
<?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.legacy.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' : '')"
t-attf-style="font-family:'font'; stroke: none; fill: {{color}};" 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">
<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="Type your name to sign" 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 position-relative mt-3 border" style="overflow: hidden;">
<div class="card-header">
<div class="row g-0 flex-nowrap">
<div class="col-auto">
<a role="button" href="#" t-attf-class="o_web_sign_auto_button me-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_draw_button me-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_load_button me-2 btn btn-light {{ widget.signMode === 'load' ? 'active': '' }}">
Load
</a>
</div>
<div class="pushend col-auto ms-auto"/>
<div t-attf-class="o_web_sign_draw_clear col-auto {{ widget.signMode !== 'draw' ? 'd-none' : '' }}">
<a role="button" href="#" class="btn btn-link fa fa-trash"/>
</div>
<div t-attf-class="o_web_sign_auto_select_style col-auto {{ widget.signMode !== 'auto' ? 'd-none' : '' }}">
<a role="button" href="#" class="btn btn-link fa fa-font"/>
</div>
<div t-attf-class="o_web_sign_load_file col-auto {{ widget.signMode !== 'load' ? 'd-none' : '' }}">
<button type="button" id="loadFileXml" onclick="document.getElementById('file').click();"
class="btn btn-link fa fa-upload" />
<input type="file" role="button" id="file" name="files[]" class="d-none"/>
</div>
</div>
</div>
<div class="o_web_sign_signature_container">
<div class="o_signature_stroke position-absolute"/>
<div class="o_web_sign_signature p-0 bg-transparent position-relative"/>
</div>
<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>
</templates>
+5
View File
@@ -16,6 +16,11 @@
@include o-text-overflow;
}
.dropdown-menu {
max-height: 70vh;
overflow: auto;
background-clip: border-box;
}
.dropdown-toggle {
white-space: nowrap;
@@ -60,12 +60,6 @@ kbd {
// == Boostrap Dropdown
// ----------------------------------------------------------------------------
.dropdown-menu {
max-height: 70vh;
overflow: auto;
background-clip: border-box;
}
:not(.dropstart) > .dropdown-item {
&.active, &.selected {
position: relative;
@@ -54,7 +54,7 @@ QUnit.module("Components", ({ beforeEach }) => {
await mount(Parent, target, { env });
assert.strictEqual(
target.querySelector(".dropdown").outerHTML,
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button class="dropdown-toggle" tabindex="0" aria-expanded="false"></button></div>'
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button type="button" class="dropdown-toggle" tabindex="0" aria-expanded="false"></button></div>'
);
assert.containsOnce(target, "button.dropdown-toggle");
assert.containsNone(target, ".dropdown-menu");
@@ -1352,7 +1352,7 @@ QUnit.module("Components", ({ beforeEach }) => {
await mount(Parent, target, { env });
assert.strictEqual(
target.querySelector(".dropdown").outerHTML,
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button class="dropdown-toggle" disabled="" tabindex="0" aria-expanded="false"></button></div>'
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button type="button" class="dropdown-toggle" disabled="" tabindex="0" aria-expanded="false"></button></div>'
);
});
});
@@ -1,81 +0,0 @@
/** @odoo-module **/
import { NameAndSignature } from "@web/legacy/js/widgets/name_and_signature";
import makeTestEnvironment from "../helpers/test_env";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { Component } from "@odoo/owl";
const MockedNameAndSignature = NameAndSignature.extend({
events: {
...NameAndSignature.prototype.events,
'signature_changed': () => {},
},
_onChangeSignature: () => {},
_drawCurrentName: () => {},
});
async function MockedNameAndSignatureGenerator (options) {
const parent = $("#qunit-fixture");
patchWithCleanup(Component, {
env: await makeTestEnvironment({}, (route) => {
if (route === "/web/sign/get_fonts/") {
return Promise.resolve();
}
}),
});
const mockedNameAndSignature = new MockedNameAndSignature(parent, options);
await mockedNameAndSignature.appendTo(parent);
await mockedNameAndSignature.resetSignature();
return mockedNameAndSignature;
}
QUnit.module('widgets legacy', {}, function () {
QUnit.module('name_and_signature', {
beforeEach: function () {
this.defaultName = 'Don Toliver'
},
}, function () {
QUnit.test("test name_and_signature widget", async function (assert) {
assert.expect(5);
const nameAndSignature = await MockedNameAndSignatureGenerator({
defaultName: this.defaultName
});
assert.equal(nameAndSignature.signMode, 'auto');
const nameInput = nameAndSignature.$el.find('.o_web_sign_name_input');
assert.ok(nameInput.length);
assert.equal(this.defaultName, nameInput.val());
const drawButton = nameAndSignature.$el.find('.o_web_sign_draw_button');
assert.ok(drawButton.length);
await drawButton.click();
assert.equal(nameAndSignature.signMode, 'draw');
});
QUnit.test("test name_and_signature widget without name", async function (assert) {
assert.expect(4);
const nameAndSignature = await MockedNameAndSignatureGenerator({});
assert.equal(nameAndSignature.signMode, 'auto');
assert.ok(nameAndSignature.signatureAreaHidden);
const nameInput = nameAndSignature.$el.find('.o_web_sign_name_input');
assert.ok(nameInput.length);
await nameInput.val(this.defaultName).trigger('input');
assert.notOk(nameAndSignature.signatureAreaHidden);
});
QUnit.test("test name_and_signature widget with noInputName and default name", async function (assert) {
assert.expect(1);
const nameAndSignature = await MockedNameAndSignatureGenerator({
noInputName: true,
defaultName: this.defaultName
});
assert.equal(nameAndSignature.signMode, 'auto');
});
QUnit.test("test name_and_signature widget with noInputName", async function (assert) {
assert.expect(1);
const nameAndSignature = await MockedNameAndSignatureGenerator({
noInputName: true,
});
assert.equal(nameAndSignature.signMode, 'draw');
});
});
});
-3
View File
@@ -45,7 +45,6 @@ Odoo Web Editor widget.
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/legacy/js/promise_extension.js',
@@ -93,8 +92,6 @@ Odoo Web Editor widget.
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
('include', 'web_editor.assets_wysiwyg'),
('include', 'web_editor.assets_legacy_wysiwyg'),
],
-3
View File
@@ -145,7 +145,6 @@ Featuring
'web/static/src/views/fields/signature/signature_field.scss',
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'website/static/src/libs/zoomodoo/zoomodoo.scss',
'web/static/src/legacy/js/promise_extension.js',
@@ -194,8 +193,6 @@ Featuring
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
'web/static/src/core/**/*.js',
'web/static/src/env.js',
'web/static/src/libs/pdfjs.js',