Skip to content

Commit f0dbb2e

Browse files
committed
[18.0][IMP] sign_oca: Add guided arrow flow to sign_oca
1 parent 9074efa commit f0dbb2e

File tree

5 files changed

+294
-0
lines changed

5 files changed

+294
-0
lines changed

sign_oca/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml",
8080
"sign_oca/static/src/elements/elements.xml",
8181
"sign_oca/static/src/scss/sign_oca.scss",
82+
"sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_navigator.esm.js",
8283
"sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js",
8384
"sign_oca/static/src/elements/text.esm.js",
8485
"sign_oca/static/src/elements/signature.esm.js",

sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,18 @@ export default class SignOcaPdfCommon extends Component {
122122
this.items[item.id] = signatureItem[0];
123123
return signatureItem;
124124
}
125+
// CheckSignItemsCompletion and navigate functions for handling navigation
126+
checkSignItemsCompletion() {
127+
const signItemsToComplete = [];
128+
$.each(this.info.items, (key, value) => {
129+
const $element = $(value);
130+
const signItemToComplete = {};
131+
signItemToComplete.data = $element[0];
132+
signItemToComplete.el = this.postIframeField(value)[0];
133+
signItemsToComplete.push(signItemToComplete);
134+
});
135+
return signItemsToComplete;
136+
}
125137
}
126138
SignOcaPdfCommon.template = "sign_oca.SignOcaPdfCommon";
127139
SignOcaPdfCommon.props = [];
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/** @odoo-module QWeb **/
2+
/* global document, window */
3+
import {_t} from "@web/core/l10n/translation";
4+
5+
export function offset(el) {
6+
const box = el.getBoundingClientRect();
7+
const docElem = document.documentElement;
8+
return {
9+
top: box.top + window.scrollY - docElem.clientTop,
10+
left: box.left + window.scrollY - docElem.clientLeft,
11+
};
12+
}
13+
14+
/**
15+
* Starts the sign item navigator
16+
* @param { SignablePDFIframe } parent
17+
* @param { HTMLElement } target
18+
* @param { Environment } env
19+
*/
20+
export function startSignItemNavigator(parent, target, env) {
21+
const state = {
22+
started: false,
23+
isScrolling: false,
24+
};
25+
const checkSignItemsCompletion = parent.checkSignItemsCompletion();
26+
let signItemsToComplete = checkSignItemsCompletion;
27+
28+
const navigator = document.createElement("div");
29+
navigator.classList.add("o_sign_sign_item_navigator");
30+
const navLine = document.createElement("div");
31+
navLine.classList.add("o_sign_sign_item_navline");
32+
33+
function _scrollToSignItemPromise(item) {
34+
return new Promise((resolve) => {
35+
if (env.isSmall) {
36+
state.isScrolling = true;
37+
item.scrollIntoView({
38+
behavior: "smooth",
39+
block: "center",
40+
inline: "center",
41+
});
42+
return resolve();
43+
}
44+
45+
state.isScrolling = true;
46+
const viewer = parent.iframe.el.contentDocument.getElementById("viewer");
47+
48+
// Recalculate container height each time
49+
const containerHeight = target.offsetHeight;
50+
const viewerHeight = viewer.offsetHeight;
51+
52+
let scrollOffset = containerHeight / 4;
53+
54+
// Get the latest scroll position
55+
const updatedScrollTop =
56+
offset(item).top - offset(viewer).top - scrollOffset;
57+
58+
// Adjust for overscroll cases
59+
if (updatedScrollTop + containerHeight > viewerHeight) {
60+
scrollOffset += updatedScrollTop + containerHeight - viewerHeight;
61+
}
62+
if (updatedScrollTop < 0) {
63+
scrollOffset += updatedScrollTop;
64+
}
65+
66+
// Ensure navigator updates properly
67+
scrollOffset +=
68+
offset(target).top -
69+
navigator.offsetHeight / 2 +
70+
item.getBoundingClientRect().height / 2;
71+
72+
// Dynamic animation duration
73+
const duration = Math.max(
74+
Math.min(
75+
500,
76+
5 *
77+
(Math.abs(target.scrollTop - updatedScrollTop) +
78+
Math.abs(navigator.getBoundingClientRect().top) -
79+
scrollOffset)
80+
),
81+
100
82+
);
83+
84+
// Perform scroll
85+
target.scrollTo({top: updatedScrollTop, behavior: "smooth"});
86+
87+
// Update navigator animation
88+
const an = navigator.animate(
89+
{top: `${scrollOffset}px`},
90+
{duration, fill: "forwards"}
91+
);
92+
const an2 = navLine.animate(
93+
{top: `${scrollOffset}px`},
94+
{duration, fill: "forwards"}
95+
);
96+
97+
Promise.all([an.finished, an2.finished]).then(() => {
98+
resolve();
99+
});
100+
});
101+
}
102+
103+
function setTip(text) {
104+
navigator.style.fontFamily = "Helvetica";
105+
navigator.innerText = text;
106+
}
107+
108+
function scrollToSignItem({el: item}) {
109+
_scrollToSignItemPromise(item).then(() => {
110+
// Define input to deal with input fields if present
111+
const input = item.querySelector("input");
112+
if (input) {
113+
if (input.type === "text") {
114+
input.focus();
115+
} else if (input.type === "checkbox") {
116+
input.focus();
117+
input.checked = true;
118+
} else {
119+
input.focus();
120+
}
121+
}
122+
// Field can be signature div or anything else
123+
else if (item.dataset.field) {
124+
const clickableElement =
125+
item.firstChild && item.querySelector("div")
126+
? item.firstChild
127+
: item;
128+
clickableElement.click();
129+
} else {
130+
item.focus();
131+
}
132+
item.classList.add("ui-selected");
133+
state.isScrolling = false;
134+
});
135+
}
136+
137+
function goToNextSignItem() {
138+
if (!state.started) {
139+
state.started = true;
140+
goToNextSignItem();
141+
return false;
142+
}
143+
const selectedElements = target.querySelectorAll(".ui-selected");
144+
selectedElements.forEach((selectedElement) => {
145+
selectedElement.classList.remove("ui-selected");
146+
});
147+
if (signItemsToComplete.length > 0) {
148+
scrollToSignItem(signItemsToComplete[0]);
149+
}
150+
}
151+
152+
target.append(navigator);
153+
navigator.before(navLine);
154+
navigator.addEventListener("click", () => {
155+
if (checkSignItemsCompletion.length > 0) {
156+
goToNextSignItem();
157+
signItemsToComplete = signItemsToComplete.slice(1);
158+
}
159+
});
160+
161+
setTip(_t("Click to start"));
162+
navigator.focus();
163+
164+
function toggle(force) {
165+
navigator.style.display = force ? "" : "none";
166+
navLine.style.display = force ? "" : "none";
167+
}
168+
169+
return {
170+
setTip,
171+
goToNextSignItem,
172+
toggle,
173+
state,
174+
};
175+
}

sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SignOcaPdf from "../sign_oca_pdf/sign_oca_pdf.esm.js";
88
import {getTemplate} from "@web/core/templates";
99
import {MainComponentsContainer} from "@web/core/main_components_container";
1010
import {rpc} from "@web/core/network/rpc";
11+
import {startSignItemNavigator} from "./sign_oca_navigator.esm";
1112

1213
export class SignOcaPdfPortal extends SignOcaPdf {
1314
setup() {
@@ -36,6 +37,8 @@ export class SignOcaPdfPortal extends SignOcaPdf {
3637
postIframeFields() {
3738
super.postIframeFields(...arguments);
3839
this.checkFilledAll();
40+
// Load navigator
41+
this.navigate();
3942
}
4043
async _onClickSign(ev) {
4144
ev.target.disabled = true;
@@ -55,6 +58,15 @@ export class SignOcaPdfPortal extends SignOcaPdf {
5558
}
5659
});
5760
}
61+
navigate() {
62+
const target = this.iframe.el.contentDocument.getElementById("viewerContainer");
63+
this.navigator = startSignItemNavigator(this, target, this.env);
64+
target.addEventListener("scroll", () => {
65+
if (!this.navigator.state.isScrolling && this.navigator.state.started) {
66+
this.navigator.setTip(_t("next"));
67+
}
68+
});
69+
}
5870
}
5971
SignOcaPdfPortal.template = "sign_oca.SignOcaPdfPortal";
6072
SignOcaPdfPortal.props = {

sign_oca/static/src/scss/sign.scss

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,97 @@
4646
background-color: rgba(0, 128, 128, 0.4);
4747
}
4848
}
49+
50+
// kobros
51+
52+
:root {
53+
--gray-200-rgb: 231, 233, 237;
54+
--gray-300-rgb: 216, 218, 221;
55+
// EE
56+
--bs-primary-rgb: 113, 75, 103;
57+
// CE
58+
--bs-primary-rgb: 113, 99, 158;
59+
--bs-secondary-rgb: var(--gray-200-rgb);
60+
--bs-danger-rgb: 212, 76, 89;
61+
--bs-dark-rgb: 17, 24, 39;
62+
--bs-white-rgb: 255, 255, 255;
63+
--bs-body-color-rgb: 55, 65, 81;
64+
--bs-body-bg-rgb: 249, 250, 251;
65+
--btn-font-weight: 500;
66+
--btn-font-size: 0.875rem;
67+
--btn-line-height: 1.5;
68+
--border-radius: 0.25rem;
69+
}
70+
71+
.o_sign_sign_item_navigator {
72+
position: fixed;
73+
top: 15%;
74+
left: 0;
75+
line-height: 50px;
76+
height: 50px;
77+
font-size: 1.4em;
78+
text-transform: uppercase;
79+
z-index: 100;
80+
padding: 0 10px 0 5px;
81+
color: white;
82+
cursor: pointer;
83+
background-color: rgba(var(--bs-primary-rgb), 1);
84+
}
85+
86+
.o_sign_sign_item_navigator:after {
87+
content: "";
88+
position: absolute;
89+
margin-left: 10px;
90+
width: 0px;
91+
height: 1px;
92+
border-top: 24px solid transparent;
93+
border-bottom: 25px solid transparent;
94+
border-left: 25px solid rgba(var(--bs-primary-rgb), 1);
95+
}
96+
97+
@media (max-width: 767px) {
98+
/* @screen-xs-max */
99+
.o_sign_sign_item_navigator {
100+
width: 100%;
101+
top: initial !important;
102+
bottom: 0;
103+
z-index: 9999;
104+
line-height: 25px;
105+
height: 35px;
106+
padding: 5px 0 0;
107+
font-size: var(--btn-font-size);
108+
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
109+
text-align: center;
110+
}
111+
.o_sign_sign_item_navline {
112+
display: none !important;
113+
}
114+
}
115+
116+
.o_sign_sign_item_navline {
117+
position: fixed;
118+
top: 15%;
119+
left: 1%;
120+
121+
pointer-events: none;
122+
z-index: 80;
123+
124+
width: 99%;
125+
height: 25px;
126+
border-bottom: 1px dashed silver;
127+
opacity: 0.5;
128+
}
129+
130+
@media (max-width: 767px) {
131+
/* @screen-xs-max */
132+
.o_sign_sign_item_navline {
133+
line-height: 12.5px;
134+
height: 12.5px;
135+
}
136+
}
137+
138+
.ui-selected,
139+
.ui-selecting {
140+
/* jQuery UI */
141+
box-shadow: 0px 0px 5px 1px orange;
142+
}

0 commit comments

Comments
 (0)