Skip to content
Snippets Groups Projects
Select Git revision
  • 25564a2461ebe5f7520a98eba1b26880015d1493
  • main default protected
  • demo protected
  • master
  • icon-set-mapping
  • production protected
  • revert-62666d1a
  • favorites-and-recent-files
  • lit2
  • wc-part
  • mark-downloaded-files
  • feature/annotpdf-test
  • fix-zip-upload
  • config-cleanup
  • wip
  • app-shell-update
16 results

ext-sign-iframe.js

Blame
  • dbp-qualified-signature-pdf-upload.js 47.80 KiB
    import {createI18nInstance} from './i18n.js';
    import {humanFileSize} from '@dbp-toolkit/common/i18next.js';
    import {css, html} from 'lit-element';
    import {ScopedElementsMixin} from '@open-wc/scoped-elements';
    import DBPSignatureLitElement from "./dbp-signature-lit-element";
    import {PdfPreview} from "./dbp-pdf-preview";
    import * as commonUtils from '@dbp-toolkit/common/utils';
    import * as utils from './utils';
    import {Button, Icon, MiniSpinner} from '@dbp-toolkit/common';
    import * as commonStyles from '@dbp-toolkit/common/styles';
    import {classMap} from 'lit-html/directives/class-map.js';
    import {FileSource} from '@dbp-toolkit/file-handling';
    import JSONLD from "@dbp-toolkit/common/jsonld";
    import {TextSwitch} from './textswitch.js';
    import {FileSink} from "@dbp-toolkit/file-handling";
    import {name as pkgName} from './../package.json';
    import {getPDFSignatureCount} from './utils.js';
    import {send as notify} from '@dbp-toolkit/common/notification';
    import metadata from './dbp-qualified-signature-pdf-upload.metadata.json';
    import {Activity} from './activity.js';
    import {PdfAnnotationView} from "./dbp-pdf-annotation-view";
    
    const i18n = createI18nInstance();
    
    class QualifiedSignaturePdfUpload extends ScopedElementsMixin(DBPSignatureLitElement) {
        constructor() {
            super();
            this.lang = i18n.language;
            this.entryPointUrl = '';
            this.nextcloudWebAppPasswordURL = "";
            this.nextcloudWebDavURL = "";
            this.nextcloudName = "";
            this.nextcloudFileURL = "";
            this.externalAuthInProgress = false;
            this.signedFiles = [];
            this.signedFilesCount = 0;
            this.signedFilesToDownload = 0;
            this.errorFiles = [];
            this.errorFilesCount = 0;
            this.uploadStatusFileName = "";
            this.uploadStatusText = "";
            this.currentFile = {};
            this.currentFileName = "";
            this.currentFilePlacementMode = "";
            this.currentFileSignaturePlacement = {};
            this.signingProcessEnabled = false;
            this.signingProcessActive = false;
            this.signaturePlacementInProgress = false;
            this.withSigBlock = false;
            this.queuedFilesSignaturePlacements = [];
            this.queuedFilesPlacementModes = [];
            this.queuedFilesNeedsPlacement = new Map();
            this.currentPreviewQueueKey = '';
            this.allowAnnotating = false;
            this.queuedFilesAnnotations = [];
            this.queuedFilesAnnotationsCount = 0;
            this.queuedFilesEnabledAnnotations = [];
            this.isAnnotationViewVisible = false;
            this.addAnnotationInProgress = false;
    
            this.activity = new Activity(metadata);
    
            this._onReceiveIframeMessage = this.onReceiveIframeMessage.bind(this);
            this._onReceiveBeforeUnload = this.onReceiveBeforeUnload.bind(this);
        }
    
        static get scopedElements() {
            return {
              'dbp-icon': Icon,
              'dbp-file-source': FileSource,
              'dbp-file-sink': FileSink,
              'dbp-pdf-preview': PdfPreview,
              'dbp-mini-spinner': MiniSpinner,
              'dbp-button': Button,
              'dbp-textswitch': TextSwitch,
              'dbp-pdf-annotation-view': PdfAnnotationView,
            };
        }
    
        static get properties() {
            return {
                ...super.properties,
                lang: { type: String },
                entryPointUrl: { type: String, attribute: 'entry-point-url' },
                nextcloudWebAppPasswordURL: { type: String, attribute: 'nextcloud-web-app-password-url' },
                nextcloudWebDavURL: { type: String, attribute: 'nextcloud-webdav-url' },
                nextcloudName: { type: String, attribute: 'nextcloud-name' },
                nextcloudFileURL: { type: String, attribute: 'nextcloud-file-url' },
                signedFiles: { type: Array, attribute: false },
                signedFilesCount: { type: Number, attribute: false },
                signedFilesToDownload: { type: Number, attribute: false },
                queuedFilesCount: { type: Number, attribute: false },
                errorFiles: { type: Array, attribute: false },
                errorFilesCount: { type: Number, attribute: false },
                uploadInProgress: { type: Boolean, attribute: false },
                uploadStatusFileName: { type: String, attribute: false },
                uploadStatusText: { type: String, attribute: false },
                externalAuthInProgress: { type: Boolean, 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 },
                allowAnnotating: { type: Boolean, attribute: 'allow-annotating' },
                isAnnotationViewVisible: { type: Boolean, attribute: false },
                queuedFilesAnnotations: { type: Array, attribute: false },
                queuedFilesAnnotationsCount: { type: Number, attribute: false },
                addAnnotationInProgress: { 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);
    
            this.updateComplete.then(()=>{
                // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
                window.addEventListener('message', this._onReceiveIframeMessage);
    
                // we want to be able to cancel the leaving of the page
                window.addEventListener('beforeunload', this._onReceiveBeforeUnload);
            });
        }
    
        disconnectedCallback() {
            // remove event listeners
            window.removeEventListener('message', this._onReceiveIframeMessage);
            window.removeEventListener('beforeunload', this._onReceiveBeforeUnload);
    
            super.disconnectedCallback();
        }
    
        async _updateNeedsPlacementStatus(id) {
            let file = this.queuedFiles[id];
            let sigCount = await getPDFSignatureCount(file);
            this.queuedFilesNeedsPlacement.delete(id);
            if (sigCount > 0)
                this.queuedFilesNeedsPlacement.set(id, true);
        }
    
        /**
         * Processes queued files
         */
        async handleQueuedFiles() {
            this.endSigningProcessIfQueueEmpty();
            if (this.queuedFilesCount === 0) {
                // reset signingProcessEnabled button
                this.signingProcessEnabled = false;
                return;
            }
    
            if (!this.signingProcessEnabled || this.externalAuthInProgress || this.uploadInProgress || this.addAnnotationInProgress) {
                return;
            }
            this.signaturePlacementInProgress = false;
    
            // Validate that all PDFs with a signature have manual placement
            for (const key of Object.keys(this.queuedFiles)) {
                const isManual = this.queuedFilesPlacementModes[key] === 'manual';
                if (this.queuedFilesNeedsPlacement.get(key) && !isManual) {
                    // Some have a signature but are not "manual", stop everything
                    notify({
                        "body": i18n.t('error-manual-positioning-missing'),
                        "type": "danger",
                        "timeout": 5,
                    });
                    this.signingProcessEnabled = false;
                    this.signingProcessActive = false;
                    return;
                }
            }
    
            // take the file off the queue
            const key = Object.keys(this.queuedFiles)[0];
            const 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) {
                    params = utils.fabricjs2pdfasPosition(data);
                }
            }
    
            this.uploadStatusText = i18n.t('qualified-pdf-upload.upload-status-file-text', {
                fileName: file.name,
                fileSize: humanFileSize(file.size, false),
            });
    
            const annotationsEnabled = this.isAnnotationsEnabledForKey(key);
            const annotations = this.takeAnnotationsFromQueue(key);
            await this.uploadFile(file, params, annotationsEnabled ? annotations : []);
            this.uploadInProgress = false;
        }
    
        storePDFData(event) {
            let placement = event.detail;
            let placementMode = 'manual';
    
            let key = this.currentPreviewQueueKey;
            this.queuedFilesSignaturePlacements[key] = placement;
            this.queuedFilesPlacementModes[key] = placementMode;
            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;
            }
            this.requestUpdate();
        }
    
        /**
         * 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('qualified-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 = '';
        }
    
        /**
         * Parse error message for user friendly output
         *
         * @param error
         */
        parseError(error) {
            let errorParsed = error;
            // Common Error Messages fpr pdf-as: https://www.buergerkarte.at/konzept/securitylayer/spezifikation/20140114/errorcodes/errorcodes.html
            // SecurityLayer Error: [6000] Unklassifizierter Abbruch durch den Bürger.
            if (error.includes('SecurityLayer Error: [6001]'))
            {
                errorParsed = i18n.t('error-cancel-message');
            }
            // SecurityLayer Error: [6001] Abbruch durch den Bürger über die Benutzerschnittstelle.
            else if (error.includes('SecurityLayer Error: [6000]'))
            {
                errorParsed = i18n.t('error-cancel-message');
            }
            // SecurityLayer Error: [6002] Abbruch auf Grund mangelnder Rechte zur Befehlsausführung.
            else if (error.includes('SecurityLayer Error: [6002]'))
            {
                errorParsed = i18n.t('error-rights-message');
            }
            return errorParsed;
        }
    
        onReceiveIframeMessage(event) {
            const data = event.data;
    
            // check if this is really a postMessage from our iframe without using event.origin
            if (data.type === 'pdf-as-error') {
                let file = this.currentFile;
                let error = data.error;
                if (data.cause) {
                    error = `${error}: ${data.cause}`;
                }
                file.json = {"hydra:description" : this.parseError(error)};
                this.addToErrorFiles(file);
                this._("#iframe").src = "about:blank";
                this.externalAuthInProgress = false;
                this.endSigningProcessIfQueueEmpty();
                return;
            }
    
            if (data.type !== 'pdf-as-callback') {
                return;
            }
    
            const sessionId = data.sessionId;
    
            // check if sessionId is valid
            if ((typeof sessionId !== 'string') || (sessionId.length < 15)) {
                return;
            }
    
            console.log("Got iframe message for sessionId " + sessionId + ", origin: " + event.origin);
            const that = this;
    
            // get correct file name
            const fileName = this.currentFileName === "" ? "mydoc.pdf" : this.currentFileName;
    
            // fetch pdf from api gateway with sessionId
            JSONLD.getInstance(this.entryPointUrl).then((jsonld) => {
                const apiUrl = jsonld.getApiUrlForEntityName("QualifiedlySignedDocument") + '/' + encodeURIComponent(sessionId) + '?fileName=' +
                    encodeURIComponent(fileName);
    
                fetch(apiUrl, {
                    headers: {
                        'Content-Type': 'application/ld+json',
                        'Authorization': 'Bearer ' + that.auth.token,
                    },
                })
                    .then(result => {
                        // hide iframe
                        that.externalAuthInProgress = false;
                        this._("#iframe").src = "about:blank";
                        this.endSigningProcessIfQueueEmpty();
    
                        if (!result.ok) throw result;
    
                        return result.json();
                    })
                    .then((document) => {
                        // this doesn't seem to trigger an update() execution
                        that.signedFiles.push(document);
                        // this triggers the correct update() execution
                        that.signedFilesCount++;
    
                        this.sendSetPropertyEvent('analytics-event', {
                            'category': 'QualifiedlySigning', 'action': 'DocumentSigned', 'name': document.contentSize});
                    }).catch(error => {
                        let file = this.currentFile;
                        // let's override the json to inject an error message
                        file.json = {"hydra:description" : "Download failed!"};
    
                        this.addToErrorFiles(file);
                    });
            }, {}, that.lang);
    
        }
    
        endSigningProcessIfQueueEmpty() {
            if (this.queuedFilesCount === 0 && this.signingProcessActive) {
                this.signingProcessActive = false;
            }
        }
    
        /**
         * @param ev
         */
        onFileSelected(ev) {
            console.log("File was selected: ev", ev);
            this.queueFile(ev.detail.file);
        }
    
        async queueFile(file) {
            let id = await super.queueFile(file);
            await this._updateNeedsPlacementStatus(id);
            this.requestUpdate();
            return id;
        }
    
        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++;
    
            this.sendSetPropertyEvent('analytics-event', {
                'category': 'QualifiedlySigning', 'action': 'SigningFailed', 'name': file.json["hydra:description"]});
        }
    
        /**
         * @param data
         */
        onFileUploadFinished(data) {
            if (data.status !== 201) {
                this.addToErrorFiles(data);
            } else if (data.json["@type"] === "http://schema.org/EntryPoint" ) {
                // after the "real" upload we immediately start with the 2FA process
    
                // show the iframe and lock processing
                this.externalAuthInProgress = true;
    
                const entryPoint = data.json;
                this.currentFileName = entryPoint.name;
    
                // we need the full file to upload it again in case the download of the signed file fails
                this.currentFile = data;
    
                // we want to load the redirect url in the iframe
                let iframe = this._("#iframe");
                iframe.src = entryPoint.url;
            }
        }
    
        update(changedProperties) {
            changedProperties.forEach((oldValue, propName) => {
                switch (propName) {
                    case "lang":
                        i18n.changeLanguage(this.lang);
                        break;
                    case "entryPointUrl":
                        JSONLD.getInstance(this.entryPointUrl).then((jsonld) => {
                            const apiUrlBase = jsonld.getApiUrlForEntityName("QualifiedSigningRequest");
                            this.fileSourceUrl = apiUrlBase ;
                        });
                        break;
                }
    
                // console.log(propName, oldValue);
            });
    
            super.update(changedProperties);
        }
    
        onLanguageChanged(e) {
            this.lang = e.detail.lang;
        }
    
        /**
         * Re-Upload all failed files
         */
        reUploadAllClickHandler() {
            const that = this;
    
            // we need to make a copy and reset the queue or else our queue will run crazy
            const errorFilesCopy = {...this.errorFiles};
            this.errorFiles = [];
            this.errorFilesCount = 0;
    
            commonUtils.asyncObjectForEach(errorFilesCopy, async (file, id) => {
                await this.fileQueueingClickHandler(file.file, id);
            });
    
            that._("#re-upload-all-button").stop();
        }
    
        /**
         * Queues a failed pdf-file again
         *
         * @param file
         * @param id
         */
        async fileQueueingClickHandler(file, id) {
            this.takeFailedFileFromQueue(id);
            return this.queueFile(file);
        }
    
        /**
         * Shows the preview
         *
         * @param key
         * @param withSigBlock
         */
        async showPreview(key, withSigBlock=false) {
            if (this.signingProcessEnabled) {
                return;
            }
    
            const file = this.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("dbp-pdf-preview");
            await this._(previewTag).showPDF(
                file,
                withSigBlock, //this.queuedFilesPlacementModes[key] === "manual",
                this.queuedFilesSignaturePlacements[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;
            return file;
        }
    
        clearQueuedFiles() {
            this.queuedFilesSignaturePlacements = [];
            this.queuedFilesPlacementModes = [];
            this.queuedFilesNeedsPlacement.clear();
            super.clearQueuedFiles();
        }
    
        clearSignedFiles() {
            this.signedFiles = [];
            this.signedFilesCount = 0;
        }
    
        clearErrorFiles() {
            this.errorFiles = [];
            this.errorFilesCount = 0;
        }
    
        isUserInterfaceDisabled() {
            return this.signaturePlacementInProgress || this.externalAuthInProgress || this.uploadInProgress || this.addAnnotationInProgress;
        }
    
        static get styles() {
            // language=css
            return css`
                ${commonStyles.getThemeCSS()}
                ${commonStyles.getGeneralCSS(false)}
                ${commonStyles.getButtonCSS()}
                ${commonStyles.getNotificationCSS()}
    
                #annotation-view .button.is-cancel {
                    background: transparent;
                    border: none;
                    font-size: 1.5rem;
                    color: var(--dbp-override-danger-bg-color);
                    cursor: pointer;
                    padding: 0px;
                    padding-right: 2px;
                }
                
                #annotation-view .box-header, #external-auth .box-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: start;
                }
    
                #annotation-view .box-header .filename, #external-auth .box-header .filename {
                    overflow: hidden;
                    text-overflow: ellipsis;
                    margin-right: 0.5em;
                }
    
                #pdf-preview, #annotation-view {
                    min-width: 320px;
                    box-sizing: border-box;
                }
    
                h2:first-child {
                    margin-top: 0;
                    margin-bottom: 0px;
                }
                
                strong {
                    font-weight: 600;
                }
    
                #pdf-preview .box-header, #annotation-view .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;
                }
    
                .error-files .file {
                    display: grid;
                    grid-template-columns: 40px auto;
                }
    
                .files-block .file .button-box {
                    display: flex;
                    align-items: center;
                }
    
                .files-block .file .info {
                    display: inline-block;
                    vertical-align: middle;
                }
    
                .file .info strong {
                    font-weight: 600;
                }
    
                .notification dbp-mini-spinner {
                    position: relative;
                    top: 2px;
                    margin-right: 5px;
                }
    
                .error, #cancel-signing-process {
                    color: #e4154b;
                }
    
                #cancel-signing-process:hover {
                    color: white;
                }
    
                /* using dbp-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 auto;
                    grid-gap: 6px;
                    margin-top: 6px;
                }
    
                .file-block .error-line {
                    margin-top: 6px;
                    color: var(--dbp-override-danger-bg-color);
                }
    
                .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 {
                    color: #e4154b;
                }
    
                #external-auth iframe {
                    margin-top: 0.5em;
                }
    
                #external-auth .button.is-cancel {
                    color: #e4154b;
                }
    
                #iframe {
                    width: 100%;
                    height: 240px;
                    /* "overflow" should not be supported by browsers, but some seem to use it */
                    overflow: hidden;
                    border-width: 0;
                    /* keeps the A-Trust webpage aligned left */
                    max-width: 575px;
                }
    
                .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;
                }
                
                #grid-container{
                    margin-top: 2rem;
                    /* padding-top: 2rem; */
                }
                
                .border{
                    border-top: 1px solid black;
                    margin-top: 2rem;
                    padding-top: 2rem;
                }
    
                .placement-missing {
                    border: solid 2px var(--dbp-override-danger-bg-color);
                }
                
                .subheadline{
                    font-style: italic;
                    padding-left: 2em;
                    margin-top: -1px;
                    /*line-height: 1.8;*/
                    margin-bottom: 1.2em;
                }
                
                
    
                /* Handling for small displays (like mobile devices) */
                @media (max-width: 680px) {
                    /* Modal preview, upload and external auth */
                    div.right-container > * {
                        position: fixed;
                        z-index: 1000;
                        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 {*[]} Array of html templates
         */
        getQueuedFilesHtml() {
            const ids = Object.keys(this.queuedFiles);
            let results = [];
    
            ids.forEach((id) => {
                const file = this.queuedFiles[id];
                const isManual = this.queuedFilesPlacementModes[id] === 'manual';
                const placementMissing = this.queuedFilesNeedsPlacement.get(id) && !isManual;
    
                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('qualified-pdf-upload.remove-queued-file-button-title')}"
                                @click="${() => { this.takeFileFromQueue(id); }}">
                                <dbp-icon name="trash"></dbp-icon></button>
                        </div>
                        <div class="bottom-line">
                            <div></div>
                            <button class="button"
                                ?disabled="${this.signingProcessEnabled}"
                                @click="${() => { this.showPreview(id); }}">${i18n.t('qualified-pdf-upload.show-preview')}</button>
                            <span class="headline">${i18n.t('qualified-pdf-upload.positioning')}:</span>
                            <dbp-textswitch name1="auto"
                                name2="manual"
                                name="${this.queuedFilesPlacementModes[id] || "auto"}"
                                class="${classMap({'placement-missing': placementMissing, 'switch': true})}"
                                value1="${i18n.t('qualified-pdf-upload.positioning-automatic')}"
                                value2="${i18n.t('qualified-pdf-upload.positioning-manual')}"
                                ?disabled="${this.signingProcessEnabled}"
                                @change=${ (e) => this.queuePlacementSwitch(id, e.target.name) }></dbp-textswitch>
                            <span class="headline ${classMap({hidden: !this.allowAnnotating})}">${i18n.t('qualified-pdf-upload.annotation')}:</span>
                            <div class="${classMap({hidden: !this.allowAnnotating})}">
                                <dbp-textswitch id="annotation-switch"
                                    name1="no-text"
                                    name2="text-selected"
                                    class="${classMap({'switch': true})}"
                                    value1="${i18n.t('qualified-pdf-upload.annotation-no')}"
                                    value2="${i18n.t('qualified-pdf-upload.annotation-yes')}"
                                    ?disabled="${this.signingProcessEnabled}"
                                    @change=${ (e) => this.showAnnotationView(id, e.target.name) }></dbp-textswitch>
                            </div>
                        </div>
                        <div class="error-line">
                            ${ placementMissing ? html`
                                ${i18n.t('label-manual-positioning-missing')}
                            ` : '' }
                        </div>
                    </div>
                `);
            });
    
            return results;
        }
    
        /**
         * Returns the list of successfully signed files
         *
         * @returns {*[]} Array of html templates
         */
        getSignedFilesHtml() {
            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('qualified-pdf-upload.download-file-button-title')}"
                                @click="${() => { this.downloadFileClickHandler(file); }}">
                                <dbp-icon name="download"></dbp-icon></button>
                        </div>
                    </div>
                `);
            });
    
            return results;
        }
    
        /**
         * Returns the list of files of failed signature processes
         *
         * @returns {*[]} Array of html templates
         */
        getErrorFilesHtml() {
            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('qualified-pdf-upload.re-upload-file-button-title')}"
                                        @click="${() => {this.fileQueueingClickHandler(data.file, id);}}"><dbp-icon name="reload"></dbp-icon></button>
                                <button class="button"
                                    title="${i18n.t('qualified-pdf-upload.remove-failed-file-button-title')}"
                                    @click="${() => { this.takeFailedFileFromQueue(id); }}">
                                    <dbp-icon name="trash"></dbp-icon></button>
                            </div>
                        </div>
                        <div class="bottom-line">
                            <strong class="error">${data.json["hydra:description"]}</strong>
                        </div>
                    </div>
                `);
            });
    
            return results;
        }
    
        hasSignaturePermissions() {
            return this._hasSignaturePermissions('ROLE_SCOPE_QUALIFIED-SIGNATURE');
        }
    
        async stopSigningProcess() {
            if (!this.externalAuthInProgress) {
                return;
            }
    
            this._("#iframe").src = "about:blank";
            this.signingProcessEnabled = false;
            this.externalAuthInProgress = false;
            this.signingProcessActive = false;
    
            if (this.currentFile.file !== undefined) {
                const key = await this.queueFile(this.currentFile.file);
    
                // set placement mode and parameters so they are restore when canceled
                this.queuedFilesPlacementModes[key] = this.currentFilePlacementMode;
                this.queuedFilesSignaturePlacements[key] = this.currentFileSignaturePlacement;
            }
        }
    
        render() {
            const placeholderUrl = commonUtils.getAssetURL(pkgName, 'qualified-signature-placeholder.png');
    
            return html`
                <div class="${classMap({hidden: !this.isLoggedIn() || !this.hasSignaturePermissions() || this.isLoading()})}">
                    <div class="field ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}">
                        <h2>${this.activity.getName(this.lang)}</h2>
                        <p class="subheadline">
                            ${this.activity.getDescription(this.lang)}
                        </p>
                        <div class="control">
                            
                            <p>
                                    ${i18n.t('qualified-pdf-upload.upload-text')}
                            </p>
                            <button @click="${() => { this._("#file-source").setAttribute("dialog-open", ""); }}"
                                    ?disabled="${this.signingProcessActive}"
                                    class="button is-primary">
                                ${i18n.t('qualified-pdf-upload.upload-button-label')}
                            </button>
                           
                            <dbp-file-source
                                id="file-source"
                                context="${i18n.t('qualified-pdf-upload.file-picker-context')}"
                                allowed-mime-types="application/pdf"
                                enabled-targets="local${this.showNextcloudFilePicker ? ",nextcloud" : ""}"
                                nextcloud-auth-url="${this.nextcloudWebAppPasswordURL}"
                                nextcloud-web-dav-url="${this.nextcloudWebDavURL}"
                                nextcloud-name="${this.nextcloudName}"
                                nextcloud-file-url="${this.nextcloudFileURL}"
                                decompress-zip
                                lang="${this.lang}"
                                ?disabled="${this.signingProcessActive}"
                                text="${i18n.t('qualified-pdf-upload.upload-area-text')}"
                                button-label="${i18n.t('qualified-pdf-upload.upload-button-label')}"
                                @dbp-file-source-file-selected="${this.onFileSelected}"
                                @dbp-file-source-switched="${this.onFileSourceSwitch}"
                                ></dbp-file-source>
                        </div>
                    </div>
                    <div id="grid-container">
                        <div class="left-container">
                            <div class="files-block field ${classMap({hidden: !this.queueBlockEnabled})}">
                                <!-- Queued files headline and queueing spinner -->
                                <h3 class="${classMap({"is-disabled": this.isUserInterfaceDisabled()})}">
                                    ${i18n.t('qualified-pdf-upload.queued-files-label')}
                                </h3>
                                <!-- 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('qualified-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('qualified-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('qualified-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('qualified-pdf-upload.queued-files-empty1')}<br />
                                    ${i18n.t('qualified-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()})}">
                                <h3>${i18n.t('qualified-pdf-upload.signed-files-label')}</h3>
                                <!-- 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('qualified-pdf-upload.clear-all')}
                                        </button>
                                        <dbp-button id="zip-download-button"
                                                    value="${i18n.t('qualified-pdf-upload.download-zip-button')}"
                                                    title="${i18n.t('qualified-pdf-upload.download-zip-button-tooltip')}"
                                                    class="is-right"
                                                    @click="${this.zipDownloadClickHandler}"
                                                    type="is-primary"></dbp-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()})}">
                                <h3>${i18n.t('qualified-pdf-upload.error-files-label')}</h3>
                                <!-- 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('qualified-pdf-upload.clear-all')}
                                        </button>
                                        <dbp-button id="re-upload-all-button"
                                                    ?disabled="${this.uploadInProgress}"
                                                    value="${i18n.t('qualified-pdf-upload.re-upload-all-button')}"
                                                    title="${i18n.t('qualified-pdf-upload.re-upload-all-button-title')}"
                                                    class="is-right"
                                                    @click="${this.reUploadAllClickHandler}"
                                                    type="is-primary"></dbp-button>
                                    </div>
                                </div>
                                <div class="control">
                                    ${this.getErrorFilesHtml()}
                                </div>
                            </div>
                        </div>
                        <div class="right-container">
                            <!-- PDF preview -->
                            <div id="pdf-preview" class="field ${classMap({hidden: !this.signaturePlacementInProgress})}">
                                <h3>${this.withSigBlock ? i18n.t('qualified-pdf-upload.signature-placement-label') : i18n.t('qualified-pdf-upload.preview-label')}</h3>
                                <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}"><dbp-icon name="close"></dbp-icon></button>
                                </div>
                                <dbp-pdf-preview lang="${this.lang}"
                                                 allow-signature-rotation
                                                 signature-placeholder-image-src="${placeholderUrl}"
                                                 signature-width="80"
                                                 signature-height="29"
                                                 @dbp-pdf-preview-accept="${this.storePDFData}"
                                                 @dbp-pdf-preview-cancel="${this.hidePDF}"></dbp-pdf-preview>
                            </div>
                            <!-- Annotation view -->
                            <div id="annotation-view" class="field ${classMap({hidden: !this.isAnnotationViewVisible || !this.allowAnnotating})}">
                                <h2>${i18n.t('qualified-pdf-upload.annotation-view-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 annotation"
                                        @click="${this.hideAnnotationView}"><dbp-icon name="close" id="close-icon"></dbp-icon></button>
                                </div>
                                <dbp-pdf-annotation-view lang="${this.lang}"
                                                 @dbp-pdf-annotations-save="${this.processAnnotationEvent}"
                                                 @dbp-pdf-annotations-cancel="${this.hideAnnotationView}"></dbp-pdf-annotation-view>
                            </div>
                            <!-- File upload progress -->
                            <div id="upload-progress" class="field notification is-info ${classMap({hidden: !this.uploadInProgress})}">
                                <dbp-mini-spinner></dbp-mini-spinner>
                                <strong>${this.uploadStatusFileName}</strong>
                                ${this.uploadStatusText}
                            </div>
                            <!-- External auth -->
                            <div id="external-auth" class="files-block field ${classMap({hidden: !this.externalAuthInProgress})}">
                                <h3>${i18n.t('qualified-pdf-upload.current-signing-process-label')}</h3>
                                <div class="box">
                                    <div class="box-header">
                                        <div class="filename">
                                            <strong>${this.currentFileName}</strong> (${humanFileSize(this.currentFile.file !== undefined ? this.currentFile.file.size : 0)})
                                        </div>
                                        <button class="button is-cancel"
                                                title="${i18n.t('qualified-pdf-upload.stop-signing-process-button')}"
                                                @click="${this.stopSigningProcess}"><dbp-icon name="close"></dbp-icon></button>
                                    </div>
                                    <!-- "scrolling" is deprecated, but still seem to help -->
                                    <iframe id="iframe" scrolling="no"></iframe>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="notification is-warning ${classMap({hidden: this.isLoggedIn() || this.isLoading()})}">
                    ${i18n.t('error-login-message')}
                </div>
                <div class="notification is-danger ${classMap({hidden: this.hasSignaturePermissions() || !this.isLoggedIn() || this.isLoading()})}">
                    ${i18n.t('error-permission-message')}
                </div>
                <div class="${classMap({hidden: !this.isLoading()})}">
                    <dbp-mini-spinner></dbp-mini-spinner>
                </div>
                <dbp-file-sink id="file-sink"
                    context="${i18n.t('qualified-pdf-upload.save-field-label', {count: this.signedFilesToDownload})}"
                    filename="signed-documents.zip"
                    subscribe="initial-file-handling-state:initial-file-handling-state"
                    enabled-targets="local${this.showNextcloudFilePicker ? ",nextcloud" : ""}"
                    nextcloud-auth-url="${this.nextcloudWebAppPasswordURL}"
                    nextcloud-web-dav-url="${this.nextcloudWebDavURL}"
                    nextcloud-name="${this.nextcloudName}"
                    nextcloud-file-url="${this.nextcloudFileURL}"
                    lang="${this.lang}"
                    ></dbp-file-sink>
            `;
        }
    }
    
    commonUtils.defineCustomElement('dbp-qualified-signature-pdf-upload', QualifiedSignaturePdfUpload);