[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:
@@ -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'),
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user