Skip to content

Commit ab74e88

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

File tree

4 files changed

+333
-0
lines changed

4 files changed

+333
-0
lines changed

sign_oca/__manifest__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
"sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml",
4141
"sign_oca/static/src/elements/elements.xml",
4242
"sign_oca/static/src/scss/sign_oca.scss",
43+
# kobros
44+
"sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_navigator.esm.js",
45+
#
4346
"sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js",
4447
"sign_oca/static/src/components/sign_oca_configure/sign_oca_configure_field_dialog.esm.js",
4548
"sign_oca/static/src/components/sign_oca_configure/sign_oca_configure_field_dialog.xml",
@@ -79,6 +82,9 @@
7982
"sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml",
8083
"sign_oca/static/src/elements/elements.xml",
8184
"sign_oca/static/src/scss/sign_oca.scss",
85+
# kobros
86+
"sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_navigator.esm.js",
87+
#
8288
"sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js",
8389
"sign_oca/static/src/elements/text.esm.js",
8490
"sign_oca/static/src/elements/signature.esm.js",
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/** @odoo-module QWeb **/
2+
/* global document, window, console */
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 { Object } types
19+
* @param { Environment } env
20+
*/
21+
export function startSignItemNavigator(parent, target, types, env) {
22+
console.log("types", types);
23+
console.log("env", env);
24+
25+
const state = {
26+
started: false,
27+
isScrolling: false,
28+
};
29+
30+
const navigator = document.createElement("div");
31+
navigator.classList.add("o_sign_sign_item_navigator");
32+
const navLine = document.createElement("div");
33+
navLine.classList.add("o_sign_sign_item_navline");
34+
35+
// Function _scrollToSignItemPromise(item) {
36+
// if (env.isSmall) {
37+
// return new Promise((resolve) => {
38+
// state.isScrolling = true;
39+
// item.scrollIntoView({
40+
// behavior: "smooth",
41+
// block: "center",
42+
// inline: "center",
43+
// });
44+
// resolve();
45+
// });
46+
// }
47+
// State.isScrolling = true;
48+
// const viewer = parent.iframe.el.contentDocument.getElementById("viewer");
49+
// const containerHeight = target.offsetHeight;
50+
// const viewerHeight = viewer.offsetHeight;
51+
52+
// const scrollOffset = containerHeight / 4;
53+
// Const scrollTop = offset(item).top - offset(viewer).top - scrollOffset;
54+
// if (scrollTop + containerHeight > viewerHeight) {
55+
// scrollOffset += scrollTop + containerHeight - viewerHeight;
56+
// }
57+
// if (scrollTop < 0) {
58+
// scrollOffset += scrollTop;
59+
// }
60+
// scrollOffset +=
61+
// offset(target).top -
62+
// navigator.offsetHeight / 2 +
63+
// item.getBoundingClientRect().height / 2;
64+
65+
// const duration = Math.max(
66+
// Math.min(
67+
// 500,
68+
// 5 *
69+
// (Math.abs(target.scrollTop - scrollTop) +
70+
// Math.abs(navigator.getBoundingClientRect().top) -
71+
// scrollOffset)
72+
// ),
73+
// 100
74+
// );
75+
76+
// return new Promise((resolve) => {
77+
// target.scrollTo({top: scrollTop, behavior: "smooth"});
78+
// target.scrollTo({behavior: "smooth"});
79+
// const an = navigator.animate(
80+
// {top: `${scrollOffset}px`},
81+
// {duration, fill: "forwards"}
82+
// );
83+
// const an2 = navLine.animate(
84+
// {top: `${scrollOffset}px`},
85+
// {duration, fill: "forwards"}
86+
// );
87+
// Promise.all([an.finished, an2.finished]).then(() => resolve());
88+
// });
89+
90+
// return new Promise((resolve, reject) => {
91+
// setTimeout(() => {
92+
// resolve("Promise resolved! ✅");
93+
// }, 2000); // Simulates an async task
94+
// });
95+
// }
96+
97+
function setTip(text) {
98+
navigator.style.fontFamily = "Helvetica";
99+
navigator.innerText = text;
100+
}
101+
102+
// /**
103+
// * Sets the entire radio set on focus.
104+
// * @param {Number} radio_set_id
105+
// */
106+
// function highligtRadioSet(radio_set_id) {
107+
// parent
108+
// .checkSignItemsCompletion()
109+
// .filter((item) => item.data.radio_set_id === radio_set_id)
110+
// .forEach((item) => {
111+
// item.el.classList.add("ui-selected");
112+
// });
113+
// }
114+
115+
function scrollToSignItem({el: item, data}) {
116+
console.log("item", item);
117+
console.log("data", data);
118+
119+
// _scrollToSignItemPromise(item).then(() => {
120+
// Const type = types[data.type_id];
121+
// if (type.item_type === "text" && item.querySelector("input")) {
122+
// item.value = item.querySelector("input").value;
123+
// item.focus = () => item.querySelector("input").focus();
124+
// }
125+
// // Maybe store signature in data rather than in the dataset
126+
// if (item.value === "" && !item.dataset.signature) {
127+
// setTip(type.tip);
128+
// }
129+
// // parent.refreshSignItems();
130+
// if (data.type === "radio") {
131+
// // We need to highligt the entire radio set items
132+
// highligtRadioSet(data.radio_set_id);
133+
// } else {
134+
// item.focus();
135+
// item.classList.add("ui-selected");
136+
// }
137+
// if (["signature", "initial"].includes(type.item_type)) {
138+
// if (item.dataset.hasFocus) {
139+
// const clickableElement = data.isSignItemEditable
140+
// ? item.querySelector(".o_sign_item_display")
141+
// : item;
142+
// clickableElement.click();
143+
// } else {
144+
// item.dataset.hasFocus = true;
145+
// }
146+
// }
147+
// state.isScrolling = false;
148+
// });
149+
}
150+
151+
function goToNextSignItem() {
152+
if (!state.started) {
153+
state.started = true;
154+
// Parent.refreshSignItems();
155+
goToNextSignItem();
156+
return false;
157+
}
158+
const selectedElements = target.querySelectorAll(".ui-selected");
159+
selectedElements.forEach((selectedElement) => {
160+
selectedElement.classList.remove("ui-selected");
161+
});
162+
163+
// I am using this function to return all fields to be signed
164+
// but in EE it returns an array of objects
165+
const signItemsToComplete2 = parent.checkSignItemsCompletion().sort((a, b) => {
166+
return (
167+
100 * (a.data.page - b.data.page) +
168+
10 * (a.data.posY - b.data.posY) +
169+
(a.data.posX - b.data.posX)
170+
);
171+
});
172+
const signItemsToComplete = [
173+
{data: {page: 1, posY: 10, posX: 20}},
174+
{data: {page: 2, posY: 5, posX: 30}},
175+
{data: {page: 1, posY: 15, posX: 25}},
176+
];
177+
178+
if (signItemsToComplete.length > 0) {
179+
const first_target_field = signItemsToComplete2[0][0];
180+
181+
console.log("first_target_field", typeof first_target_field);
182+
console.log("first_target_field", first_target_field);
183+
184+
scrollToSignItem(signItemsToComplete[0]);
185+
first_target_field.append(navigator);
186+
}
187+
}
188+
189+
navigator.addEventListener("click", goToNextSignItem);
190+
191+
goToNextSignItem();
192+
// Target.append(navigator);
193+
navigator.before(navLine);
194+
195+
setTip(_t("Click to start"));
196+
navigator.focus();
197+
198+
function toggle(force) {
199+
navigator.style.display = force ? "" : "none";
200+
navLine.style.display = force ? "" : "none";
201+
}
202+
203+
return {
204+
setTip,
205+
goToNextSignItem,
206+
toggle,
207+
state,
208+
};
209+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Component, onMounted, onWillStart, onWillUnmount, useRef} from "@odoo/ow
55
import {AlertDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
66
import {renderToString} from "@web/core/utils/render";
77
import {useService} from "@web/core/utils/hooks";
8+
import {startSignItemNavigator} from "./sign_oca_navigator.esm";
89

910
export default class SignOcaPdfCommon extends Component {
1011
setup() {
@@ -104,6 +105,9 @@ export default class SignOcaPdfCommon extends Component {
104105
"sign_oca_ready"
105106
);
106107
this.iframeLoaded.resolve();
108+
// Kobros
109+
this.navigate();
110+
//
107111
}
108112
postIframeField(item) {
109113
if (this.items[item.id]) {
@@ -122,6 +126,32 @@ export default class SignOcaPdfCommon extends Component {
122126
this.items[item.id] = signatureItem[0];
123127
return signatureItem;
124128
}
129+
130+
// Kobros
131+
checkSignItemsCompletion() {
132+
const signItemsToComplete = [];
133+
$.each(this.info.items, (key) => {
134+
const signItemToComplete = this.postIframeField(this.info.items[key]);
135+
signItemsToComplete.push(signItemToComplete);
136+
});
137+
return signItemsToComplete;
138+
}
139+
navigate() {
140+
const target = this.iframe.el.contentDocument.getElementById("viewerContainer");
141+
this.navigator = startSignItemNavigator(
142+
this,
143+
target,
144+
// I assume type is text but it can be many things
145+
[{type_id: {item_type: "text"}}],
146+
this.env
147+
);
148+
target.addEventListener("scroll", () => {
149+
if (!this.navigator.state.isScrolling && this.navigator.state.started) {
150+
this.navigator.setTip(_t("next"));
151+
}
152+
});
153+
}
154+
//
125155
}
126156
SignOcaPdfCommon.template = "sign_oca.SignOcaPdfCommon";
127157
SignOcaPdfCommon.props = [];

sign_oca/static/src/scss/sign.scss

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,91 @@
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+
}

0 commit comments

Comments
 (0)