Merge branch '318-signature-vue-app-show-full-doc' into 'master'

Resolve "[App de signature] Pouvoir voir le document en continu (toutes les pages ensemble)"

Closes #318

See merge request Chill-Projet/chill-bundles!761
This commit is contained in:
Julien Fastré 2024-12-05 16:31:04 +00:00
commit f5c1b5cf8a
4 changed files with 168 additions and 52 deletions

View File

@ -0,0 +1,5 @@
kind: Feature
body: Show all the pages of the documents in the signature app
time: 2024-12-05T17:23:41.866322287+01:00
custom:
Issue: "318"

View File

@ -0,0 +1,11 @@
chill_main:
workflow_signature:
base_signer:
document_kinds:
- { key: id_card, labels: [ { lang: fr, label: "Carte d'identité" } ] }
- { key: passport, labels: [ { lang: fr, label: "Passeport" } ] }
- { key: drivers_license, labels: [ { lang: fr, label: "Permis de conduire" } ] }
- { key: visa_long_stay, labels: [ { lang: fr, label: "Visa de long séjour" } ] }
- { key: resident_permit, labels: [ { lang: fr, label: "Carte de séjour" } ] }
- { key: residency_card, labels: [ { lang: fr, label: "Carte de résident" } ] }
- { key: provisionary_residency_permit, labels: [ { lang: fr, label: "Autorisation provisoire de séjour" } ] }

View File

@ -26,9 +26,9 @@
</template> </template>
</modal> </modal>
</teleport> </teleport>
<div class="col-12 m-auto"> <div class="col-12 m-auto sticky-top">
<div class="row justify-content-center border-bottom pdf-tools d-md-none"> <div class="row justify-content-center border-bottom pdf-tools d-md-none">
<div class="col text-center turn-page"> <div class="col-5 text-center turn-page">
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="zoomSelect" id="zoomSelect"
@ -57,9 +57,35 @@
</button> </button>
</template> </template>
<template v-if="pageCount > 1">
<button
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
<input
type="checkbox"
id="checkboxMulti"
v-model="multiPage"
@change="toggleMultiPage"
/>
<label class="form-check-label" for="checkboxMulti">
{{ $t("all_pages") }}
</label>
</template>
</div> </div>
<div <div
v-if="signature.zones.length > 1" v-if="signature.zones.length > 0"
class="col-5 p-0 text-center turnSignature" class="col-5 p-0 text-center turnSignature"
> >
<button <button
@ -120,7 +146,7 @@
<div <div
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex" class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
> >
<div class="col-3 text-center turn-page ps-3"> <div class="col-5 text-center turn-page ps-3">
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="zoomSelect" id="zoomSelect"
@ -148,10 +174,19 @@
> >
</button> </button>
<input
type="checkbox"
id="checkboxMulti"
v-model="multiPage"
@change="toggleMultiPage"
/>
<label class="form-check-label" for="checkboxMulti">
{{ $t("see_all_pages") }}
</label>
</template> </template>
</div> </div>
<div <div
v-if="signature.zones.length > 1 && signedState !== 'signed'" v-if="signature.zones.length > 0 && signedState !== 'signed'"
class="col-4 d-xl-none text-center turnSignature p-0" class="col-4 d-xl-none text-center turnSignature p-0"
> >
<button <button
@ -171,7 +206,7 @@
</button> </button>
</div> </div>
<div <div
v-if="signature.zones.length > 1 && signedState !== 'signed'" v-if="signature.zones.length > 0 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature" class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
> >
<button <button
@ -233,12 +268,19 @@
</div> </div>
</div> </div>
<div <div
v-if="multiPage"
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center d-flex flex-column"
:class="{ onAddZone: canvasEvent === 'add' }"
>
<canvas v-for="p in pageCount" :key="p" :id="`canvas-${p}`"></canvas>
</div>
<div
v-else
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center" class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center"
:class="{ onAddZone: canvasEvent === 'add' }" :class="{ onAddZone: canvasEvent === 'add' }"
> >
<canvas class="m-auto" id="canvas"></canvas> <canvas class="m-auto" id="canvas"></canvas>
</div> </div>
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons"> <div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
<div class="row"> <div class="row">
<div class="col d-flex"> <div class="col d-flex">
@ -298,6 +340,7 @@ import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButt
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs"; pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
const multiPage: Ref<boolean> = ref(true);
const modalOpen: Ref<boolean> = ref(false); const modalOpen: Ref<boolean> = ref(false);
const loading: Ref<boolean> = ref(false); const loading: Ref<boolean> = ref(false);
const adding: Ref<boolean> = ref(false); const adding: Ref<boolean> = ref(false);
@ -364,23 +407,37 @@ const $toast = useToast();
const signature = window.signature; const signature = window.signature;
const setZoomLevel = (zoomLevel: string) => { const setZoomLevel = async (zoomLevel: string) => {
zoom.value = Number.parseFloat(zoomLevel); zoom.value = Number.parseFloat(zoomLevel);
setPage(page.value); await resetPages();
setTimeout(() => drawAllZones(page.value), 200); setTimeout(drawAllZones, 200);
}; };
const mountPdf = async (doc: ArrayBuffer) => { const mountPdf = async (doc: ArrayBuffer) => {
const loadingTask = pdfjsLib.getDocument(doc); const loadingTask = pdfjsLib.getDocument(doc);
pdf = await loadingTask.promise; pdf = await loadingTask.promise;
pageCount.value = pdf.numPages; pageCount.value = pdf.numPages;
await setPage(page.value); if (multiPage.value) {
await setAllPages();
} else {
await setPage(1);
}
};
const getCanvas = (page: number) =>
multiPage.value
? (document.getElementById(`canvas-${page}`) as HTMLCanvasElement)
: (document.querySelectorAll("canvas")[0] as HTMLCanvasElement);
const getCanvasId = (canvas: HTMLCanvasElement) => {
const number = canvas.id.split("-").pop();
return number ? parseInt(number) : 0;
}; };
const getRenderContext = (pdfPage: PDFPageProxy) => { const getRenderContext = (pdfPage: PDFPageProxy) => {
const scale = 1 * zoom.value; const scale = 1 * zoom.value;
const viewport = pdfPage.getViewport({ scale }); const viewport = pdfPage.getViewport({ scale });
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement; const canvas = getCanvas(pdfPage.pageNumber);
const context = canvas.getContext("2d") as CanvasRenderingContext2D; const context = canvas.getContext("2d") as CanvasRenderingContext2D;
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
@ -391,6 +448,9 @@ const getRenderContext = (pdfPage: PDFPageProxy) => {
}; };
}; };
const setAllPages = async () =>
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
const setPage = async (page: number) => { const setPage = async (page: number) => {
const pdfPage = await pdf.getPage(page); const pdfPage = await pdf.getPage(page);
const renderContext = getRenderContext(pdfPage); const renderContext = getRenderContext(pdfPage);
@ -412,10 +472,34 @@ async function downloadAndOpen(): Promise<Blob> {
return raw; return raw;
} }
const initPdf = () => { const addCanvasEvents = () => {
if (multiPage.value) {
Array.from(Array(pageCount.value).keys()).map((p) => {
const canvas = getCanvas(p + 1);
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false
);
});
} else {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement; const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvas.addEventListener("pointerup", canvasClick, false); canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
setTimeout(() => drawAllZones(page.value), 800); }
};
const initPdf = () => {
addCanvasEvents();
setTimeout(drawAllZones, 800);
};
const resetPages = () =>
multiPage.value ? setAllPages() : setPage(page.value);
const toggleMultiPage = async () => {
await resetPages();
setTimeout(drawAllZones, 200);
addCanvasEvents();
}; };
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) => const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
@ -427,35 +511,36 @@ const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
const hitSignature = ( const hitSignature = (
zone: SignatureZone, zone: SignatureZone,
xy: number[], xy: number[],
canvasWidth: number, canvas: HTMLCanvasElement
canvasHeight: number
) => ) =>
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] && scaleXToCanvas(zone.x, canvas.width, zone.PDFPage.width) < xy[0] &&
xy[0] < xy[0] <
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) && scaleXToCanvas(zone.x + zone.width, canvas.width, zone.PDFPage.width) &&
zone.PDFPage.height * zoom.value - zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) < scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
xy[1] && xy[1] &&
xy[1] < xy[1] <
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) + scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
zone.PDFPage.height * zoom.value; zone.PDFPage.height * zoom.value;
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => { const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
userSignatureZone.value = z; userSignatureZone.value = z;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
setPage(page.value); await resetPages();
setTimeout(() => drawAllZones(page.value), 200); setTimeout(drawAllZones, 200);
} }
}; };
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) => const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones signature.zones
.filter((z) => z.PDFPage.index + 1 === page.value) .filter(
(z) =>
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
)
.map((z) => { .map((z) => {
if ( if (hitSignature(z, [e.offsetX, e.offsetY], canvas)) {
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
) {
if (userSignatureZone.value === null) { if (userSignatureZone.value === null) {
selectZone(z, canvas); selectZone(z, canvas);
} else { } else {
@ -466,18 +551,20 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
} }
}); });
const canvasClick = (e: PointerEvent) => { const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvasEvent.value === "select" canvasEvent.value === "select"
? selectZoneEvent(e, canvas) ? selectZoneEvent(e, canvas)
: addZoneEvent(e, canvas); : addZoneEvent(e, canvas);
};
const turnPage = async (upOrDown: number) => { const turnPage = async (upOrDown: number) => {
//userSignatureZone.value = null; // desactivate the reset of the zone when turning page
page.value = page.value + upOrDown; page.value = page.value + upOrDown;
if (multiPage.value) {
const canvas = getCanvas(page.value);
canvas.scrollIntoView();
} else {
await setPage(page.value); await setPage(page.value);
setTimeout(() => drawAllZones(page.value), 200); setTimeout(drawAllZones, 200);
}
}; };
const turnSignature = async (upOrDown: number) => { const turnSignature = async (upOrDown: number) => {
@ -493,9 +580,9 @@ const turnSignature = async (upOrDown: number) => {
let currentZone = signature.zones[zoneIndex]; let currentZone = signature.zones[zoneIndex];
if (currentZone) { if (currentZone) {
page.value = currentZone.PDFPage.index + 1; page.value = currentZone.PDFPage.index + 1;
userSignatureZone.value = currentZone; const canvas = getCanvas(currentZone.PDFPage.index + 1);
const canvas = document.querySelectorAll("canvas")[0];
selectZone(currentZone, canvas); selectZone(currentZone, canvas);
canvas.scrollIntoView();
} }
}; };
@ -540,13 +627,18 @@ const drawZone = (
} }
}; };
const drawAllZones = (page: number) => { const drawAllZones = () => {
const canvas = document.querySelectorAll("canvas")[0]; if (signedState.value !== "signed") {
const ctx = canvas.getContext("2d");
if (ctx && signedState.value !== "signed") {
signature.zones signature.zones
.filter((z) => z.PDFPage.index + 1 === page) .filter(
(z) =>
multiPage.value ||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
)
.map((z) => { .map((z) => {
const canvas = getCanvas(z.PDFPage.index + 1);
const ctx = canvas.getContext("2d");
if (ctx) {
if (userSignatureZone.value) { if (userSignatureZone.value) {
if (userSignatureZone.value?.index === z.index) { if (userSignatureZone.value?.index === z.index) {
drawZone(z, ctx, canvas.width, canvas.height); drawZone(z, ctx, canvas.width, canvas.height);
@ -554,6 +646,7 @@ const drawAllZones = (page: number) => {
} else { } else {
drawZone(z, ctx, canvas.width, canvas.height); drawZone(z, ctx, canvas.width, canvas.height);
} }
}
}); });
} }
}; };
@ -638,8 +731,8 @@ const confirmSign = () => {
const undoSign = async () => { const undoSign = async () => {
signature.zones = signature.zones.filter((z) => z.index !== null); signature.zones = signature.zones.filter((z) => z.index !== null);
await setPage(page.value); await resetPages();
setTimeout(() => drawAllZones(page.value), 200); setTimeout(drawAllZones, 200);
userSignatureZone.value = null; userSignatureZone.value = null;
adding.value = false; adding.value = false;
canvasEvent.value = "select"; canvasEvent.value = "select";
@ -671,7 +764,7 @@ const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
width: BOX_WIDTH * zoom.value, width: BOX_WIDTH * zoom.value,
height: BOX_HEIGHT * zoom.value, height: BOX_HEIGHT * zoom.value,
PDFPage: { PDFPage: {
index: page.value - 1, index: multiPage.value ? getCanvasId(canvas) - 1 : page.value - 1,
width: PDFPageWidth, width: PDFPageWidth,
height: PDFPageHeight, height: PDFPageHeight,
}, },
@ -679,8 +772,8 @@ const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
signature.zones.push(newZone); signature.zones.push(newZone);
userSignatureZone.value = newZone; userSignatureZone.value = newZone;
await setPage(page.value); await resetPages();
setTimeout(() => drawAllZones(page.value), 200); setTimeout(drawAllZones, 200);
canvasEvent.value = "select"; canvasEvent.value = "select";
adding.value = true; adding.value = true;
}; };
@ -695,14 +788,15 @@ init();
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
#canvas { canvas {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
margin: 1rem auto;
} }
.onAddZone { .onAddZone {
cursor: not-allowed; cursor: not-allowed;
#canvas { canvas {
cursor: copy; cursor: copy;
} }
} }
@ -716,6 +810,10 @@ div#action-buttons {
div.pdf-tools { div.pdf-tools {
background-color: #f3f3f3; background-color: #f3f3f3;
font-size: 0.6rem; font-size: 0.6rem;
label {
font-size: 0.75rem !important;
margin: auto 0 auto 0.3rem;
}
button { button {
font-size: 0.75rem !important; font-size: 0.75rem !important;
} }

View File

@ -24,6 +24,8 @@ const appMessages = {
loading: 'Chargement...', loading: 'Chargement...',
remove_sign_zone: 'Enlever la zone', remove_sign_zone: 'Enlever la zone',
return: 'Retour', return: 'Retour',
see_all_pages: 'Voir toutes les pages',
all_pages: 'Toutes les pages',
} }
} }