diff --git a/assets/official-signature-placeholder.png b/assets/official-signature-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..74a60188d4ec6dca162549dd63919a5a3a506e1b Binary files /dev/null and b/assets/official-signature-placeholder.png differ diff --git a/rollup.config.js b/rollup.config.js index db470c6b5d7c06de5f3f4806ec2758b07a67de11..2befe42b7faecde74c150285dc9c7f472c1511e4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -256,7 +256,7 @@ Dependencies: {src: 'node_modules/vpu-common/src/spinner.js', dest: 'dist/local/' + pkg.name, rename: 'spinner.js'}, {src: 'node_modules/vpu-common/misc/browser-check.js', dest: 'dist/local/' + pkg.name, rename: 'browser-check.js'}, {src: 'assets/icon-*.png', dest: 'dist/local/' + pkg.name}, - {src: 'assets/signature-placeholder.png', dest: 'dist/local/' + pkg.name}, + {src: 'assets/*-placeholder.png', dest: 'dist/local/' + pkg.name}, {src: 'assets/manifest.json', dest: 'dist', rename: pkg.name + '.manifest.json'}, {src: 'assets/*.metadata.json', dest: 'dist'}, {src: 'node_modules/vpu-common/assets/icons/*.svg', dest: 'dist/local/vpu-common/icons'}, diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json index eab8194ad5e737f6c413cf3cc9888618cbaaf588..eebc71a6a136da980ce571c960058881d04ec542 100644 --- a/src/i18n/de/translation.json +++ b/src/i18n/de/translation.json @@ -2,6 +2,22 @@ "official-pdf-upload": { "upload-field-label": "PDF-Dokumente zum Signieren hochladen", "upload-area-text": "Sie können in diesem Bereich PDF-Dokumente per Drag & Drop oder per Direktauswahl hochladen", + "current-signing-process-label": "Aktueller Signaturprozess", + "queued-files-label": "Dokumente in der Warteschlange", + "queued-files-empty1": "Kein Dokument in der Warteschlange", + "queued-files-empty2": "Sie können jetzt ein neues Dokument hochladen", + "remove-failed-file-button-title": "Fehlgeschlagenes Dokument entfernen", + "remove-queued-file-button-title": "Dokument aus der Warteschlange entfernen", + "clear-all": "Alle entfernen", + "start-signing-process-button": "Signatur starten", + "stop-signing-process-button": "Signieren unterbrechen", + "show-preview": "Dokument anzeigen", + "positioning-automatic": "Automatisch", + "positioning-manual": "Manuell", + "close-preview": "Dokumentansicht schließen", + "preview-label": "Dokumentenansicht", + "start-signing-process-button-running-title": "Signaturvorgang läuft gerade", + "remove-current-file-button-title": "Aktuellen Signaturprozess abbrechen", "signed-files-label": "Signierte Dokumente", "download-zip-button": "Als ZIP Datei herunterladen", "download-zip-button-tooltip": "Alle signierten Dokumente als ZIP Datei herunterladen", @@ -11,7 +27,11 @@ "re-upload-file-button-title": "Erneut hochladen", "upload-status-file-text": "({{fileSize}}) wird hochgeladen und verarbeitet...", "re-upload-all-button": "Alle erneut hochladen", - "re-upload-all-button-title": "Alle fehlgeschlagen Uploads erneut hochladen" + "re-upload-all-button-title": "Alle fehlgeschlagen Uploads erneut hochladen", + "signature-placement-label": "Signatur platzieren", + "positioning": "Positionierung", + "file-label": "Dokument", + "confirm-page-leave": "Sind Sie sicher, dass Sie die Seite verlassen wollen? Es stehen signierte Dokumente zum Download bereit." }, "qualified-pdf-upload": { "upload-field-label": "PDF-Dokumente zum Signieren hochladen", diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 60d33f23528b6fbd4145480db8e5ac67c813c42a..b2036da7294dcdc4e37caa14d61e5ec44b857ff4 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -2,6 +2,22 @@ "official-pdf-upload": { "upload-field-label": "Upload PDF-files to sign", "upload-area-text": "In this area you can upload PDF-files via Drag & Drop or by selecting them directly", + "current-signing-process-label": "Current signing process", + "queued-files-label": "Queued documents", + "queued-files-empty1": "No queued documents", + "queued-files-empty2": "You can now upload more documents", + "remove-failed-file-button-title": "Remove failed document", + "remove-queued-file-button-title": "Remove document from queue", + "clear-all": "Clear all", + "start-signing-process-button": "Start signing", + "stop-signing-process-button": "Stop signing", + "show-preview": "Show document", + "positioning-automatic": "Automatic", + "positioning-manual": "Manual", + "close-preview": "Close preview", + "preview-label": "Document view", + "start-signing-process-button-running-title": "Signing process running", + "remove-current-file-button-title": "Cancel current signing process", "signed-files-label": "Signed files", "download-zip-button": "Download ZIP", "download-zip-button-tooltip": "Download all signed files as ZIP file", @@ -11,7 +27,11 @@ "re-upload-file-button-title": "Upload again", "upload-status-file-text": "({{fileSize}}) is currently uploading and being processed...", "re-upload-all-button": "Upload all", - "re-upload-all-button-title": "Upload all failed uploads again" + "re-upload-all-button-title": "Upload all failed uploads again", + "signature-placement-label": "Place signature", + "positioning": "Positioning", + "file-label": "document", + "confirm-page-leave": "Are you sure you want to leave this page? There are still signed documents ready to be downloaded." }, "qualified-pdf-upload": { "upload-field-label": "Upload PDF-documents to sign", diff --git a/src/vpu-official-signature-pdf-upload.js b/src/vpu-official-signature-pdf-upload.js index 69c07d2c754039fa00bf39381d6448d6148b44c0..2a1274521613b6544f7570c218cb68608323f64e 100644 --- a/src/vpu-official-signature-pdf-upload.js +++ b/src/vpu-official-signature-pdf-upload.js @@ -3,15 +3,16 @@ import {humanFileSize} from 'vpu-common/i18next.js'; import {css, html} from 'lit-element'; import {ScopedElementsMixin} from '@open-wc/scoped-elements'; import VPUSignatureLitElement from "./vpu-signature-lit-element"; +import {PdfPreview} from "./vpu-pdf-preview"; import * as commonUtils from 'vpu-common/utils'; -import {Icon, MiniSpinner, Button} from 'vpu-common'; import * as utils from './utils'; -import JSZip from 'jszip/dist/jszip.js'; +import {Icon, MiniSpinner, Button} from 'vpu-common'; import FileSaver from 'file-saver'; import * as commonStyles from 'vpu-common/styles'; import {classMap} from 'lit-html/directives/class-map.js'; import {FileUpload} from 'vpu-file-upload'; import JSONLD from "vpu-common/jsonld"; +import {TextSwitch} from './textswitch.js'; const i18n = createI18nInstance(); @@ -20,24 +21,45 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem super(); this.lang = i18n.language; this.entryPointUrl = commonUtils.getAPiUrl(); + this.signedFiles = []; this.signedFilesCount = 0; this.errorFiles = []; this.errorFilesCount = 0; this.uploadInProgress = false; + this.queueingInProgress = false; this.uploadStatusFileName = ""; this.uploadStatusText = ""; + this.currentFile = {}; + this.currentFileName = ""; + this.currentFilePlacementMode = ""; + this.currentFileSignaturePlacement = {}; + this.queueBlockEnabled = false; + this.queuedFiles = []; + this.queuedFilesCount = 0; + this.signingProcessEnabled = false; + this.signingProcessActive = false; + this.signaturePlacementInProgress = false; + this.withSigBlock = false; + this.queuedFilesSignaturePlacements = []; + this.queuedFilesPlacementModes = []; + this.currentPreviewQueueKey = ''; // will be set in function update + this.signingUrl = ""; + + } static get scopedElements() { return { 'vpu-icon': Icon, 'vpu-fileupload': FileUpload, + 'vpu-pdf-preview': PdfPreview, 'vpu-mini-spinner': MiniSpinner, 'vpu-button': Button, + 'vpu-textswitch': TextSwitch, }; } @@ -47,31 +69,300 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem entryPointUrl: { type: String, attribute: 'entry-point-url' }, signedFiles: { type: Array, attribute: false }, signedFilesCount: { type: Number, attribute: false }, + queuedFilesCount: { type: Number, attribute: false }, errorFiles: { type: Array, attribute: false }, errorFilesCount: { type: Number, attribute: false }, uploadInProgress: { type: Boolean, attribute: false }, + queueingInProgress: { type: Boolean, attribute: false }, uploadStatusFileName: { type: String, attribute: false }, uploadStatusText: { type: String, attribute: false }, + + signingProcessEnabled: { type: Boolean, attribute: false }, + signingProcessActive: { type: Boolean, attribute: false }, + queueBlockEnabled: { type: Boolean, attribute: false }, + currentFile: { type: Object, attribute: false }, + currentFileName: { type: String, attribute: false }, + signaturePlacementInProgress: { type: Boolean, attribute: false }, + withSigBlock: { type: Boolean, attribute: false }, + isSignaturePlacement: { type: Boolean, attribute: false }, }; } connectedCallback() { super.connectedCallback(); + // needs to be called in a function to get the variable scope of "this" + setInterval(() => { this.handleQueuedFiles(); }, 1000); + + + + + + + + + } + + /* disconnectedCallback() + + + + + + */ + + onQueuedFilesChanged(ev) { + const detail = ev.detail; + if (!this.queueBlockEnabled && detail.queuedFilesCount) + this.queueBlockEnabled = true; + this.queuedFiles = detail.queuedFiles; + this.queuedFilesCount = detail.queuedFilesCount; + } + + /** + * Processes queued files + */ + async handleQueuedFiles() { + + this.endSigningProcessIfQueueEmpty(); + + if (this.queuedFilesCount === 0) { + // reset signingProcessEnabled button + this.signingProcessEnabled = false; + + return; + } + + if (!this.signingProcessEnabled || this.uploadInProgress) { + return; + } + + this.signaturePlacementInProgress = false; + + const key = Object.keys(this.queuedFiles)[0]; + + // take the file off the queue + let file = this.takeFileFromQueue(key); + this.currentFile = file; + + // set placement mode and parameters to restore them when canceled + this.currentFilePlacementMode = this.queuedFilesPlacementModes[key]; + this.currentFileSignaturePlacement = this.queuedFilesSignaturePlacements[key]; + + this.uploadInProgress = true; + let params = {}; + + // prepare parameters to tell PDF-AS where and how the signature should be placed + if (this.queuedFilesPlacementModes[key] === "manual") { + const data = this.queuedFilesSignaturePlacements[key]; + + if (data !== undefined) { + let angle = data.angle; + let bottom = data.bottom; + let left = data.left; + + if (angle !== 0) { + // attempt to adapt positioning in the rotated states to fit PDF-AS + switch (angle) { + case 90: + // 321 / 118; + bottom += data.width / 2.72034; + left -= data.width / 2.72034; + break; + case 180: + // 321 / 237; + bottom += data.width / 1.3544; + break; + case 270: + left += data.height; + bottom += data.height; + break; + } + + // adapt rotation to fit PDF-AS + const rotations = {0: 0, 90: 270, 180: 180, 270: 90}; + angle = rotations[data.angle]; + } + + params = { + y: Math.round(bottom), + x: Math.round(left), + r: angle, + w: Math.round(data.width), // only width, no "height" allowed in PDF-AS + p: data.currentPage + }; + } + } + + await this._("#file-upload").uploadFile(file, params); + this.uploadInProgress = false; + } + + storePDFData(event) { + const data = event.detail; + + this.queuedFilesSignaturePlacements[this.currentPreviewQueueKey] = data; + this.signaturePlacementInProgress = false; + } + + /** + * Called when preview is "canceled" + * + * @param event + */ + hidePDF(event) { + // reset placement mode to "auto" if no placement was confirmed previously + if (this.queuedFilesSignaturePlacements[this.currentPreviewQueueKey] === undefined) { + this.queuedFilesPlacementModes[this.currentPreviewQueueKey] = "auto"; + } + + this.signaturePlacementInProgress = false; + } + + queuePlacementSwitch(key, name) { + this.queuedFilesPlacementModes[key] = name; + console.log(name); + + if (name === "manual") { + this.showPreview(key, true); + } else if (this.currentPreviewQueueKey === key) { + this.signaturePlacementInProgress = false; + } + } + + /** + * Decides if the "beforeunload" event needs to be canceled + * + * @param event + */ + onReceiveBeforeUnload(event) { + // we don't need to stop if there are no signed files + if (this.signedFilesCount === 0) { + return; + } + + // we need to handle custom events ourselves + if (!event.isTrusted) { + // note that this only works with custom event since calls of "confirm" are ignored + // in the non-custom event, see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + const result = confirm(i18n.t('official-pdf-upload.confirm-page-leave')); + + // don't stop the page leave if the user wants to leave + if (result) { + return; + } + } + + // Cancel the event as stated by the standard + event.preventDefault(); + + // Chrome requires returnValue to be set + event.returnValue = ''; + } + + /* onReceiveIframeMessage(event) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + */ + + endSigningProcessIfQueueEmpty() { + if (this.queuedFilesCount === 0 && this.signingProcessActive) { + this.signingProcessActive = false; + } } /** * @param ev */ onAllUploadStarted(ev) { - console.log("Start upload process!"); - this.uploadInProgress = true; + console.log("Start queuing process!"); + this.queueingInProgress = true; + } + + /** + * @param ev + */ + onAllUploadFinished(ev) { + console.log("Finished queuing process!"); + this.queueingInProgress = false; } /** * @param ev */ onFileUploadStarted(ev) { - console.log(ev); this.uploadStatusFileName = ev.detail.fileName; this.uploadStatusText = i18n.t('official-pdf-upload.upload-status-file-text', { fileName: ev.detail.fileName, @@ -79,29 +370,41 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem }); } + addToErrorFiles(file) { + this.endSigningProcessIfQueueEmpty(); + + // this doesn't seem to trigger an update() execution + this.errorFiles[Math.floor(Math.random() * 1000000)] = file; + // this triggers the correct update() execution + this.errorFilesCount++; + + if (window._paq !== undefined) { + window._paq.push(['trackEvent', 'officiallySigning', 'SigningFailed', file.json["hydra:description"]]); + } + } + /** * @param ev */ onFileUploadFinished(ev) { if (ev.detail.status !== 201) { - // this doesn't seem to trigger an update() execution - this.errorFiles[Math.floor(Math.random() * 1000000)] = ev.detail; - // this triggers the correct update() execution - this.errorFilesCount++; + this.addToErrorFiles(ev.detail); } else if (ev.detail.json["@type"] === "http://schema.org/MediaObject" ) { // this doesn't seem to trigger an update() execution this.signedFiles.push(ev.detail.json); // this triggers the correct update() execution this.signedFilesCount++; - } - } - /** - * @param ev - */ - onAllUploadFinished(ev) { - console.log("Finished upload process!"); - this.uploadInProgress = false; + + const entryPoint = data.json; + this.currentFileName = entryPoint.name; + + + + + + this.endSigningProcessIfQueueEmpty(); + } } update(changedProperties) { @@ -113,12 +416,13 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem case "entryPointUrl": JSONLD.initialize(this.entryPointUrl, (jsonld) => { const apiUrlBase = jsonld.getApiUrlForEntityName("OfficiallySignedDocument"); + this.signingUrl = apiUrlBase + "/sign"; }); break; } - console.log(propName, oldValue); + // console.log(propName, oldValue); }); super.update(changedProperties); @@ -131,8 +435,9 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem /** * Download signed pdf-files as zip */ - zipDownloadClickHandler() { + async zipDownloadClickHandler() { // see: https://stuk.github.io/jszip/ + let JSZip = (await import('jszip/dist/jszip.js')).default; let zip = new JSZip(); const that = this; let fileNames = []; @@ -150,12 +455,9 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem zip.file(fileName, utils.getPDFFileBase64Content(file), {base64: true}); }); - zip.generateAsync({type:"blob"}) - .then(function(content) { - FileSaver.saveAs(content, "signed-documents.zip"); - - that._("#zip-download-button").stop(); - }); + let content = await zip.generateAsync({type:"blob"}); + FileSaver.saveAs(content, "signed-documents.zip"); + that._("#zip-download-button").stop(); } /** @@ -189,17 +491,82 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem } /** - * Uploads a failed pdf-file again + * Queues a failed pdf-file again * * @param file * @param id */ - async fileUploadClickHandler(file, id) { - this.uploadInProgress = true; - this.errorFiles.splice(id, 1); + async fileQueueingClickHandler(file, id) { + this.takeFailedFileFromQueue(id); + + return this._("#file-upload").queueFile(file); + } + + /** + * Shows the preview + * + * @param key + * @param withSigBlock + */ + async showPreview(key, withSigBlock=false) { + if (this.signingProcessEnabled) { + return; + } + + const file = this._("#file-upload").getQueuedFile(key); + this.currentFile = file; + this.currentPreviewQueueKey = key; + console.log(file); + // start signature placement process + this.signaturePlacementInProgress = true; + this.withSigBlock = withSigBlock; + + const previewTag = this.constructor.getScopedTagName("vpu-pdf-preview"); + await this._(previewTag).showPDF( + file, + withSigBlock, //this.queuedFilesPlacementModes[key] === "manual", + this.queuedFilesSignaturePlacements[key]); + } + + /** + * Takes a file off of the queue + * + * @param key + */ + takeFileFromQueue(key) { + return this._("#file-upload").takeFileFromQueue(key); + } + + /** + * Takes a failed file off of the queue + * + * @param key + */ + takeFailedFileFromQueue(key) { + const file = this.errorFiles.splice(key, 1); this.errorFilesCount = Object.keys(this.errorFiles).length; - await this._("#file-upload").uploadFile(file); - this.uploadInProgress = false; + + return file; + } + + clearQueuedFiles() { + this.queuedFilesSignaturePlacements = []; + this.queuedFilesPlacementModes = []; + this._("#file-upload").clearQueuedFiles(); + } + + clearSignedFiles() { + this.signedFiles = []; + this.signedFilesCount = 0; + } + + clearErrorFiles() { + this.errorFiles = []; + this.errorFilesCount = 0; + } + + isUserInterfaceDisabled() { + return this.signaturePlacementInProgress || this.externalAuthInProgress || this.uploadInProgress; } static get styles() { @@ -210,14 +577,46 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem ${commonStyles.getButtonCSS()} ${commonStyles.getNotificationCSS()} + #pdf-preview { + min-width: 320px; + } + + h2:first-child { + margin-top: 0; + } + + h2 { + margin-bottom: 10px; + } + strong { font-weight: 600; } + #pdf-preview .box-header { + border: 1px solid #000; + border-bottom-width: 0; + padding: 0.5em 0.5em 0 0.5em; + } + .hidden { display: none; } + + + + + + + + + + + .files-block.field:not(:last-child) { + margin-bottom: 40px; + } + .files-block .file { margin: 10px 0; } @@ -247,84 +646,453 @@ class OfficialSignaturePdfUpload extends ScopedElementsMixin(VPUSignatureLitElem margin-right: 5px; } - .error { + .error, #cancel-signing-process { color: #e4154b; } + + #cancel-signing-process:hover { + color: white; + } + + /* using vpu-icon doesn't work */ + button > [name=close], a > [name=close] { + font-size: 0.8em; + } + + a > [name=close] { + color: red; + } + + .empty-queue { + margin: 10px 0; + } + + #grid-container { + display: flex; + flex-flow: row wrap; + } + + #grid-container > div { + margin-right: 20px; + } + + #grid-container > div:last-child { + margin-right: 0; + flex: 1 0; + } + + .file-block { + max-width: 320px; + } + + .file-block, .box { + border: solid 1px black; + padding: 10px; + } + + .file-block, .box .file { + margin-top: 0; + } + + .file-block { + margin-bottom: 10px; + } + + .file-block .header { + display: grid; + align-items: center; + grid-template-columns: auto 40px; + grid-gap: 10px; + } + + .file-block.error .header { + grid-template-columns: auto 80px; + } + + .file-block.error .header .buttons { + white-space: nowrap; + } + + .file-block div.bottom-line { + display: grid; + align-items: center; + grid-template-columns: auto 190px; + grid-gap: 10px; + margin-top: 10px; + } + + .file-block.error div.bottom-line { + display: block; + } + + .file-block div.bottom-line .headline { + text-align: right; + } + + .file-block .filename, .file-block div.bottom-line .headline { + text-overflow: ellipsis; + overflow: hidden; + } + + .file-block .filename { + white-space: nowrap; + } + + #pdf-preview .button.is-cancel, #external-auth .button.is-cancel { + color: #e4154b; + } + + #external-auth iframe { + margin-top: 0.5em; + } + + .is-right { + float: right; + } + + .error-files .header { + color: black; + } + + /* prevent hovering of disabled default button */ + .button[disabled]:not(.is-primary):hover { + background-color: inherit; + color: inherit; + } + + .is-disabled, .is-disabled.button[disabled] { + opacity: 0.2; + pointer-events: none; + } + + #pdf-preview .box-header, #external-auth .box-header { + display: flex; + justify-content: space-between; + align-items: start; + } + + #pdf-preview .box-header .filename, #external-auth .box-header .filename { + overflow: hidden; + text-overflow: ellipsis; + margin-right: 0.5em; + } + + /* Handling for small displays (like mobile devices) */ + @media (max-width: 680px) { + /* Modal preview, upload and external auth */ + div.right-container > * { + position: fixed; + padding: 10px; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: white; + overflow-y: scroll; + } + + /* Don't use the whole screen for the upload progress */ + #upload-progress { + top: 10px; + left: 10px; + right: 10px; + bottom: inherit; + } + + #grid-container > div { + margin-right: 0; + width: 100%; + } + + .file-block { + max-width: inherit; + } + } `; } + /** + * Returns the list of queued files + * + * @returns {*[]} + */ + getQueuedFilesHtml() { + const ids = Object.keys(this.queuedFiles); + let results = []; + + ids.forEach((id) => { + const file = this.queuedFiles[id]; + + results.push(html` + <div class="file-block"> + <div class="header"> + <span class="filename"><strong>${file.name}</strong> (${humanFileSize(file.size)})</span> + <button class="button close" + ?disabled="${this.signingProcessEnabled}" + title="${i18n.t('official-pdf-upload.remove-queued-file-button-title')}" + @click="${() => { this.takeFileFromQueue(id); }}"> + <vpu-icon name="trash"></vpu-icon></button> + </div> + <div class="bottom-line"> + <div></div> + <button class="button" + ?disabled="${this.signingProcessEnabled}" + @click="${() => { this.showPreview(id); }}">${i18n.t('official-pdf-upload.show-preview')}</button> + <span class="headline">${i18n.t('official-pdf-upload.positioning')}:</span> + <vpu-textswitch name1="auto" + name2="manual" + name="${this.queuedFilesPlacementModes[id] || "auto"}" + class="switch" + value1="${i18n.t('official-pdf-upload.positioning-automatic')}" + value2="${i18n.t('official-pdf-upload.positioning-manual')}" + ?disabled="${this.signingProcessEnabled}" + @change=${ (e) => this.queuePlacementSwitch(id, e.target.name) }></vpu-textswitch> + </div> + </div> + `); + }); + + return results; + } + + /** + * Returns the list of successfully signed files + * + * @returns {*[]} + */ getSignedFilesHtml() { - return this.signedFiles.map(file => html` - <div class="file"> - <a class="is-download" - title="${i18n.t('official-pdf-upload.download-file-button-title')}" - @click="${() => {this.fileDownloadClickHandler(file);}}"> - <strong>${file.name}</strong> (${humanFileSize(file.contentSize)}) <vpu-icon name="download"></vpu-icon></a> - </div> - `); + const ids = Object.keys(this.signedFiles); + let results = []; + + ids.forEach((id) => { + const file = this.signedFiles[id]; + + results.push(html` + <div class="file-block"> + <div class="header"> + <span class="filename"><strong>${file.name}</strong> (${humanFileSize(file.contentSize)})</span> + <button class="button close" + title="${i18n.t('official-pdf-upload.download-file-button-title')}" + @click="${() => { this.fileDownloadClickHandler(file); }}"> + <vpu-icon name="download"></vpu-icon></button> + </div> + </div> + `); + }); + + return results; } + /** + * Returns the list of files of failed signature processes + * + * @returns {*[]} + */ getErrorFilesHtml() { - return this.errorFiles.map((data, id) => html` - <div class="file"> - <div class="button-box"> - <button class="button is-small" - title="${i18n.t('official-pdf-upload.re-upload-file-button-title')}" - @click="${() => {this.fileUploadClickHandler(data.file, id);}}"><vpu-icon name="reload"></vpu-icon></button> - </div> - <div class="info"> - <strong>${data.file.name}</strong> (${humanFileSize(data.file.size)}) - <strong class="error">${data.json["hydra:description"]}</strong> + const ids = Object.keys(this.errorFiles); + let results = []; + + ids.forEach((id) => { + const data = this.errorFiles[id]; + + results.push(html` + <div class="file-block error"> + <div class="header"> + <span class="filename"><strong>${data.file.name}</strong> (${humanFileSize(data.file.size)})</span> + <div class="buttons"> + <button class="button" + title="${i18n.t('official-pdf-upload.re-upload-file-button-title')}" + @click="${() => {this.fileQueueingClickHandler(data.file, id);}}"><vpu-icon name="reload"></vpu-icon></button> + <button class="button" + title="${i18n.t('official-pdf-upload.remove-failed-file-button-title')}" + @click="${() => { this.takeFailedFileFromQueue(id); }}"> + <vpu-icon name="trash"></vpu-icon></button> + </div> + </div> + <div class="bottom-line"> + <strong class="error">${data.json["hydra:description"]}</strong> + </div> </div> - </div> - `); + `); + }); + + return results; } hasSignaturePermissions() { return this._hasSignaturePermissions('ROLE_SCOPE_OFFICIAL-SIGNATURE'); } + /* stopSigningProcess() + + + + + + + + + + + + + + + + + */ + render() { return html` <div class="${classMap({hidden: !this.isLoggedIn() || !this.hasSignaturePermissions() || this.isLoading()})}"> <div class="field"> <h2>${i18n.t('official-pdf-upload.upload-field-label')}</h2> <div class="control"> - <vpu-fileupload id="file-upload" lang="${this.lang}" url="${this.signingUrl}" accept="application/pdf" + <vpu-fileupload id="file-upload" + allowed-mime-types="application/pdf" + decompress-zip + always-send-file + deferred + lang="${this.lang}" + url="${this.signingUrl}" + ?disabled="${this.signingProcessActive}" text="${i18n.t('official-pdf-upload.upload-area-text')}" button-label="${i18n.t('official-pdf-upload.upload-button-label')}" @vpu-fileupload-all-start="${this.onAllUploadStarted}" @vpu-fileupload-file-start="${this.onFileUploadStarted}" @vpu-fileupload-file-finished="${this.onFileUploadFinished}" @vpu-fileupload-all-finished="${this.onAllUploadFinished}" + @vpu-fileupload-queued-files-changed="${this.onQueuedFilesChanged}" ></vpu-fileupload> </div> </div> - <div class="field notification is-info ${classMap({hidden: !this.uploadInProgress})}"> - <vpu-mini-spinner></vpu-mini-spinner> - <strong>${this.uploadStatusFileName}</strong> - ${this.uploadStatusText} - </div> - <div class="files-block field ${classMap({hidden: this.signedFilesCount === 0})}"> - <h2>${i18n.t('official-pdf-upload.signed-files-label')}</h2> - <div class="control"> - ${this.getSignedFilesHtml()} + <div id="grid-container"> + <div class="left-container"> + <div class="files-block field ${classMap({hidden: !this.queueBlockEnabled})}"> + <!-- Queued files headline and queueing spinner --> + <h2 class="${classMap({"is-disabled": this.isUserInterfaceDisabled()})}"> + ${i18n.t('official-pdf-upload.queued-files-label')} + <vpu-mini-spinner id="queueing-in-progress-spinner" + style="font-size: 0.7em" + class="${classMap({hidden: !this.queueingInProgress})}"></vpu-mini-spinner> + </h2> + <!-- Buttons to start/stop signing process and clear queue --> + <div class="control field"> + <button @click="${this.clearQueuedFiles}" + ?disabled="${this.queuedFilesCount === 0 || this.signingProcessActive || this.isUserInterfaceDisabled()}" + class="button ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}"> + ${i18n.t('official-pdf-upload.clear-all')} + </button> + <button @click="${() => { this.signingProcessEnabled = true; this.signingProcessActive = true; }}" + ?disabled="${this.queuedFilesCount === 0}" + class="button is-right is-primary ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}"> + ${i18n.t('official-pdf-upload.start-signing-process-button')} + </button> + <!-- --> + <button @click="${this.stopSigningProcess}" + ?disabled="${this.uploadInProgress}" + id="cancel-signing-process" + class="button is-right ${classMap({hidden: !this.signingProcessActive})}"> + ${i18n.t('official-pdf-upload.stop-signing-process-button')} + </button> + <!-- --> + </div> + <!-- List of queued files --> + <div class="control file-list ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}"> + ${this.getQueuedFilesHtml()} + </div> + <!-- Text "queue empty" --> + <div class="empty-queue control ${classMap({hidden: this.queuedFilesCount !== 0, "is-disabled": this.isUserInterfaceDisabled()})}"> + ${i18n.t('official-pdf-upload.queued-files-empty1')}<br /> + ${i18n.t('official-pdf-upload.queued-files-empty2')} + </div> + </div> + <!-- List of signed PDFs --> + <div class="files-block field ${classMap({hidden: this.signedFilesCount === 0, "is-disabled": this.isUserInterfaceDisabled()})}"> + <h2>${i18n.t('official-pdf-upload.signed-files-label')}</h2> + <!-- Button to download all signed PDFs --> + <div class="field ${classMap({hidden: this.signedFilesCount === 0})}"> + <div class="control"> + <button @click="${this.clearSignedFiles}" + class="button"> + ${i18n.t('official-pdf-upload.clear-all')} + </button> + <vpu-button id="zip-download-button" + value="${i18n.t('official-pdf-upload.download-zip-button')}" + title="${i18n.t('official-pdf-upload.download-zip-button-tooltip')}" + class="is-right" + @click="${this.zipDownloadClickHandler}" + type="is-primary"></vpu-button> + </div> + </div> + <div class="control"> + ${this.getSignedFilesHtml()} + </div> + </div> + <!-- List of errored files --> + <div class="files-block error-files field ${classMap({hidden: this.errorFilesCount === 0, "is-disabled": this.isUserInterfaceDisabled()})}"> + <h2>${i18n.t('official-pdf-upload.error-files-label')}</h2> + <!-- Button to upload errored files again --> + <div class="field ${classMap({hidden: this.errorFilesCount === 0})}"> + <div class="control"> + <button @click="${this.clearErrorFiles}" + class="button"> + ${i18n.t('official-pdf-upload.clear-all')} + </button> + <vpu-button id="re-upload-all-button" + ?disabled="${this.uploadInProgress}" + value="${i18n.t('official-pdf-upload.re-upload-all-button')}" + title="${i18n.t('official-pdf-upload.re-upload-all-button-title')}" + class="is-right" + @click="${this.reUploadAllClickHandler}" + type="is-primary"></vpu-button> + </div> + </div> + <div class="control"> + ${this.getErrorFilesHtml()} + </div> + </div> </div> - </div> - <div class="field ${classMap({hidden: this.signedFilesCount === 0})}"> - <div class="control"> - <vpu-button id="zip-download-button" value="${i18n.t('official-pdf-upload.download-zip-button')}" title="${i18n.t('official-pdf-upload.download-zip-button-tooltip')}" @click="${this.zipDownloadClickHandler}" type="is-primary"></vpu-button> - </div> - </div> - <div class="files-block error-files field ${classMap({hidden: this.errorFilesCount === 0})}"> - <h2 class="error">${i18n.t('official-pdf-upload.error-files-label')}</h2> - <div class="control"> - ${this.getErrorFilesHtml()} - </div> - </div> - <div class="field ${classMap({hidden: this.errorFilesCount === 0})}"> - <div class="control"> - <vpu-button id="re-upload-all-button" ?disabled="${this.uploadInProgress}" value="${i18n.t('official-pdf-upload.re-upload-all-button')}" title="${i18n.t('official-pdf-upload.re-upload-all-button-title')}" @click="${this.reUploadAllClickHandler}" type="is-primary"></vpu-button> + <div class="right-container"> + <!-- PDF preview --> + <div id="pdf-preview" class="field ${classMap({hidden: !this.signaturePlacementInProgress})}"> + <h2>${this.withSigBlock ? i18n.t('official-pdf-upload.signature-placement-label') : i18n.t('official-pdf-upload.preview-label')}</h2> + <div class="box-header"> + <div class="filename"> + <strong>${this.currentFile.name}</strong> (${humanFileSize(this.currentFile !== undefined ? this.currentFile.size : 0)}) + </div> + <button class="button is-cancel" + @click="${this.hidePDF}"><vpu-icon name="close"></vpu-icon></button> + </div> + <vpu-pdf-preview lang="${this.lang}" + signature-placeholder-image="official-signature-placeholder.png" + signature-width="147" + signature-height="29" + @vpu-pdf-preview-accept="${this.storePDFData}" + @vpu-pdf-preview-cancel="${this.hidePDF}"></vpu-pdf-preview> + </div> + <!-- File upload progress --> + <div id="upload-progress" class="field notification is-info ${classMap({hidden: !this.uploadInProgress})}"> + <vpu-mini-spinner></vpu-mini-spinner> + <strong>${this.uploadStatusFileName}</strong> + ${this.uploadStatusText} + </div> + <!-- External auth --> + + + + + + + + + + + + + + + </div> </div> </div> diff --git a/src/vpu-pdf-preview.js b/src/vpu-pdf-preview.js index ea9e2cf47e2fbf9393cac34f0dfcd0991749a0db..76445cf02250c84462a2fa461f3ef81b8a71a8b4 100644 --- a/src/vpu-pdf-preview.js +++ b/src/vpu-pdf-preview.js @@ -29,6 +29,9 @@ export class PdfPreview extends ScopedElementsMixin(VPULitElement) { this.fabricCanvas = null; this.canvasToPdfScale = 1.0; this.currentPageOriginalHeight = 0; + this.placeholder = 'signature-placeholder.png'; + this.signature_width = 80; + this.signature_height = 29; this._onWindowResize = this._onWindowResize.bind(this); } @@ -51,6 +54,9 @@ export class PdfPreview extends ScopedElementsMixin(VPULitElement) { isPageRenderingInProgress: { type: Boolean, attribute: false }, isPageLoaded: { type: Boolean, attribute: false }, isShowPlacement: { type: Boolean, attribute: false }, + placeholder: { type: String, attribute: 'signature-placeholder-image' }, + signature_width: { type: Number, attribute: 'signature-width' }, + signature_height: { type: Number, attribute: 'signature-height' }, }; } @@ -99,7 +105,7 @@ export class PdfPreview extends ScopedElementsMixin(VPULitElement) { }); // add signature image - fabric.Image.fromURL(commonUtils.getAssetURL('local/vpu-signature/signature-placeholder.png'), function(image) { + fabric.Image.fromURL(commonUtils.getAssetURL('local/vpu-signature/' + this.placeholder), function(image) { // add a red border around the signature placeholder image.set({stroke: "#e4154b", strokeWidth: 8}); @@ -288,7 +294,7 @@ export class PdfPreview extends ScopedElementsMixin(VPULitElement) { // set the initial position of the signature if (initSignature) { - const sigSizeMM = {width: 80, height: 29}; + const sigSizeMM = {width: this.signature_width, height: this.signature_height}; const sigPosMM = {top: 5, left: 5}; const inchPerMM = 0.03937007874; @@ -369,7 +375,7 @@ export class PdfPreview extends ScopedElementsMixin(VPULitElement) { } /** - * Rotates the signature clock-wise in 90� steps + * Rotates the signature clock-wise in 90� steps */ async rotateSignature() { let signature = this.getSignatureRect();