From 693ae15d252abfa53226f5496aecd710327c5885 Mon Sep 17 00:00:00 2001
From: Tamara Steinwender <tamara.steinwender@tugraz.at>
Date: Thu, 8 Apr 2021 09:52:44 +0200
Subject: [PATCH] Outsourcing the file-handling clipboard part to an external
 webcomponent

---
 .../src/dbp-file-handling-clipboard.js        | 494 +++++++++++++++++-
 packages/file-handling/src/file-sink.js       | 203 ++-----
 packages/file-handling/src/file-source.js     | 288 ++--------
 .../src/i18n/de/translation.json              |   5 +
 .../src/i18n/en/translation.json              |   5 +
 5 files changed, 583 insertions(+), 412 deletions(-)

diff --git a/packages/file-handling/src/dbp-file-handling-clipboard.js b/packages/file-handling/src/dbp-file-handling-clipboard.js
index 7edac72e..db3cb055 100644
--- a/packages/file-handling/src/dbp-file-handling-clipboard.js
+++ b/packages/file-handling/src/dbp-file-handling-clipboard.js
@@ -5,15 +5,32 @@ import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
 import {Icon, MiniSpinner} from '@dbp-toolkit/common';
 import * as commonStyles from '@dbp-toolkit/common/styles';
 import * as fileHandlingStyles from './styles';
+import Tabulator from "tabulator-tables";
+import {humanFileSize} from "@dbp-toolkit/common/i18next";
+import {classMap} from 'lit-html/directives/class-map.js';
+import * as commonUtils from "@dbp-toolkit/common/utils";
+import {name as pkgName} from "../package.json";
+import {send} from "@dbp-toolkit/common/notification";
+
 
 /**
- * NextcloudFilePicker web component
+ * Clipboard web component
  */
 export class FileHandlingClipboard extends ScopedElementsMixin(DBPLitElement) {
     constructor() {
         super();
         this.lang = 'de';
         this.authUrl = '';
+        this.allowedMimeTypes = '*/*';
+        this.clipboardSource = false;
+        this.clipboardFiles = {files: ''};
+        this.clipboardSelectBtnDisabled = true;
+        this.clipboardSelectBtnDisabled = true;
+        this.showSelectAllButton = true;
+        this.tabulatorTable = null;
+        this.maxSelectedItems = true;
+        this._onReceiveBeforeUnload = this.onReceiveBeforeUnload.bind(this);
+
     }
 
     static get scopedElements() {
@@ -31,6 +48,12 @@ export class FileHandlingClipboard extends ScopedElementsMixin(DBPLitElement) {
             ...super.properties,
             lang: { type: String },
             authUrl: { type: String, attribute: 'auth-url' },
+            allowedMimeTypes: { type: String, attribute: 'allowed-mime-types' },
+            showSelectAllButton: { type: Boolean, attribute: true },
+            clipboardSelectBtnDisabled: { type: Boolean, attribute: true },
+            clipboardFiles: {type: Object, attribute: 'clipboard-files'},
+            clipboardSource: {type: Boolean, attribute: 'clipboard-source'},
+
         };
 
     }
@@ -41,19 +64,291 @@ export class FileHandlingClipboard extends ScopedElementsMixin(DBPLitElement) {
                 case "lang":
                     i18n.changeLanguage(this.lang);
                     break;
+                case "clipboardFiles":
+                    this.generateClipboardTable();
+                    break;
             }
+            console.log("source", this.clipboardSource);
         });
 
         super.update(changedProperties);
     }
 
     disconnectedCallback() {
+
+        //We doesn't want to deregister this event, because we want to use this event over activities
+        //window.removeEventListener('beforeunload', this._onReceiveBeforeUnload);
+
+        super.disconnectedCallback();
     }
 
     connectedCallback() {
+        super.connectedCallback();
+        const that = this;
+        this.updateComplete.then(() => {
+
+            // see: http://tabulator.info/docs/4.7
+            this.tabulatorTable = new Tabulator(this._("#clipboard-content-table"), {
+                layout: "fitColumns",
+                selectable: this.maxSelectedItems,
+                selectableRangeMode: "drag",
+                responsiveLayout: true,
+                placeholder: i18n.t('nextcloud-file-picker.no-data-type'),
+                resizableColumns: false,
+                columns: [
+                    {
+                        title: "",
+                        field: "type",
+                        align: "center",
+                        headerSort: false,
+                        width: 50,
+                        responsive: 1,
+                        formatter: (cell, formatterParams, onRendered) => {
+                            const icon_tag = that.constructor.getScopedTagName("dbp-icon");
+                            let icon = `<${icon_tag} name="empty-file" class="nextcloud-picker-icon"></${icon_tag}>`;
+                            return icon;
+                        }
+                    },
+                    {
+                        title: i18n.t('nextcloud-file-picker.filename'),
+                        responsive: 0,
+                        widthGrow: 5,
+                        minWidth: 150,
+                        field: "name",
+                        sorter: "alphanum",
+                        formatter: (cell) => {
+                            let data = cell.getRow().getData();
+                            if (data.edit) {
+                                cell.getElement().classList.add("fokus-edit");
+                            }
+                            return cell.getValue();
+                        }
+                    },
+                    {
+                        title: i18n.t('nextcloud-file-picker.size'),
+                        responsive: 4,
+                        widthGrow: 1,
+                        minWidth: 50,
+                        field: "size",
+                        formatter: (cell, formatterParams, onRendered) => {
+                            return cell.getRow().getData().type === "directory" ? "" : humanFileSize(cell.getValue());
+                        }
+                    },
+                    {
+                        title: i18n.t('nextcloud-file-picker.mime-type'),
+                        responsive: 2,
+                        widthGrow: 1,
+                        minWidth: 20,
+                        field: "type",
+                        formatter: (cell, formatterParams, onRendered) => {
+                            if (typeof cell.getValue() === 'undefined') {
+                                return "";
+                            }
+                            const [, fileSubType] = cell.getValue().split('/');
+                            return fileSubType;
+                        }
+                    },
+                    {
+                        title: i18n.t('nextcloud-file-picker.last-modified'),
+                        responsive: 3,
+                        widthGrow: 1,
+                        minWidth: 150,
+                        field: "lastModified",
+                        sorter: (a, b, aRow, bRow, column, dir, sorterParams) => {
+                            const a_timestamp = Date.parse(a);
+                            const b_timestamp = Date.parse(b);
+                            return a_timestamp - b_timestamp;
+                        },
+                        formatter: function (cell, formatterParams, onRendered) {
+                            const timestamp = new Date(cell.getValue());
+                            const year = timestamp.getFullYear();
+                            const month = ("0" + (timestamp.getMonth() + 1)).slice(-2);
+                            const date = ("0" + timestamp.getDate()).slice(-2);
+                            const hours = ("0" + timestamp.getHours()).slice(-2);
+                            const minutes = ("0" + timestamp.getMinutes()).slice(-2);
+                            return date + "." + month + "." + year + " " + hours + ":" + minutes;
+                        }
+                    },
+                    {title: "file", field: "file", visible: false}
+                ],
+                initialSort: [
+                    {column: "name", dir: "asc"},
+                    {column: "type", dir: "asc"},
+                ],
+                rowClick: (e, row) => {
+                    if (this.tabulatorTable !== null
+                        && this.tabulatorTable.getSelectedRows().length === this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())).length) {
+                        this.showSelectAllButton = false;
+                    } else {
+                        this.showSelectAllButton = true;
+                    }
+                },
+                rowSelectionChanged: (data, rows) => {
+                    if (this.tabulatorTable && this.tabulatorTable.getSelectedRows().length > 0) {
+                        this.clipboardSelectBtnDisabled = false;
+                    } else {
+                        this.clipboardSelectBtnDisabled = true;
+                    }
+                }
+            });
+        });
+        window.addEventListener('beforeunload', this._onReceiveBeforeUnload);
+
+    }
+
+    /**
+     * Select all files from tabulator table
+     *
+     */
+    selectAll() {
+        this.tabulatorTable.selectRow(this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())));
+        if (this.tabulatorTable.getSelectedRows().length > 0) {
+            this.showSelectAllButton = false;
+            console.log("Show Select All Button:", this.showSelectAllButton);
+        }
+    }
+
+    /**
+     * Deselect files from tabulator table
+     *
+     */
+    deselectAll() {
+        this.tabulatorTable.deselectRow();
+        this.showSelectAllButton = true;
+        console.log("Show Select All Button:", this.showSelectAllButton);
+    }
+
+    checkFileType(file) {
+        // check if file is allowed
+        const [fileMainType, fileSubType] = file.type.split('/');
+        const mimeTypes = this.allowedMimeTypes.split(',');
+        let deny = true;
+
+        mimeTypes.forEach((str) => {
+            const [mainType, subType] = str.split('/');
+            deny = deny && ((mainType !== '*' && mainType !== fileMainType) || (subType !== '*' && subType !== fileSubType));
+        });
+
+        if (deny) {
+            console.log(`mime type ${file.type} of file '${file.name}' is not compatible with ${this.allowedMimeTypes}`);
+            return false;
+        }
+        return true;
+    }
+
+    generateClipboardTable() {
+        let data = [];
+        for (let i = 0; i < this.clipboardFiles.files.length; i++){
+            data[i] = {
+                name: this.clipboardFiles.files[i].name,
+                size: this.clipboardFiles.files[i].size,
+                type: this.clipboardFiles.files[i].type,
+                lastModified: this.clipboardFiles.files[i].lastModified,
+                file: this.clipboardFiles.files[i]
+            };
+        }
+
+        if (this.tabulatorTable !== null){
+            this.tabulatorTable.clearData();
+            this.tabulatorTable.setData(data);
+        }
+    }
+
+    async sendClipboardFiles(files) {
+
+        for(let i = 0; i < files.length; i ++)
+        {
+            await this.sendFileEvent(files[i].file);
+        }
+        this.tabulatorTable.deselectRow();
+        this.closeDialog();
+
+    }
+
+    async sendFileEvent(file) {
+        const data = {"file": file, "data": file};
+
+        const event = new CustomEvent("dbp-clipboard-file-picker-file-downloaded",
+            { "detail": data, bubbles: true, composed: true });
+        this.dispatchEvent(event);
+    }
+
+    /**
+     * 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.clipboardFiles.files.length === 0) {
+            return;
+        }
+
+        send({
+            "summary": i18n.t('clipboard.file-warning'),
+            "body": i18n.t('clipboard.file-warning-body', {count: this.clipboardFiles.files.length}),
+            "type": "warning",
+            "timeout": 5,
+        });
+
+        // we need to handle custom events ourselves
+        if(event.target && event.target.activeElement && event.target.activeElement.nodeName) {
+
+            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('page-leaving-warn-dialogue'));
+                // 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 = '';
+        }
+    }
+
+    saveFilesToClipboard()
+    {
+        //save it
+        let data = {};
+        if (this.files.length !== 0) {
+            data = {"files": this.files};
+            this.sendSetPropertyEvent('clipboard-files', data);
+            this.closeDialog();
+            send({
+                "summary": i18n.t('file-sink.save-to-clipboard-title'),
+                "body": i18n.t('file-sink.save-to-clipboard-body', {count: this.files.length}),
+                "type": "success",
+                "timeout": 5,
+            });
+            console.log("--------------", this.clipboardFiles);
+        }
+
     }
 
 
+
+    getClipboardFileList() {
+        let files = [];
+        for(let i = 0; i < this.clipboardFiles.files.length; i ++)
+        {
+            files[i] =  html`<div class="clipboard-list"><strong>${this.clipboardFiles.files[i].name}</strong> ${humanFileSize(this.clipboardFiles.files[i].size)}</div>`;
+        }
+        return files;
+    }
+
+    /**
+     * Open Filesink for multiple files
+     */
+    async openClipboardFileSink() {
+        this._("#file-sink-clipboard").files = this.clipboardFiles.files;
+        this._("#file-sink-clipboard").setAttribute("dialog-open", "");
+    }
+
     static get styles() {
         // language=css
         return css`
@@ -63,14 +358,201 @@ export class FileHandlingClipboard extends ScopedElementsMixin(DBPLitElement) {
             ${commonStyles.getModalDialogCSS()}
             ${commonStyles.getRadioAndCheckboxCss()}
             ${fileHandlingStyles.getFileHandlingCss()}
-            
+
+            .clipboard-container{
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                padding: var(--FUPadding, 20px);
+                width: 100%;
+                height: 100%;
+                position: relative;
+            }
+
+            .clipboard-container .wrapper{
+                overflow-y: auto;
+                text-align: center;
+                width: 100%;
+                height: 100%;
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+            }
+
+            .clipboard-container .wrapper.table{
+                justify-content: start;
+            }
+
+            .clipboard-container .wrapper .inner{
+                overflow-y: auto;
+                text-align: center;
+                width: 100%;
+            }
+
+            .clipboard-footer{
+                align-self: end;
+            }
+
+            #select-all-wrapper{
+                text-align: right;
+            }
+
+            .clipboard-container{
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                padding: var(--FUPadding, 20px);
+            }
+
+            .clipboard-container.table{
+                justify-content: start;
+            }
+
+            .clipboard-container .inner{
+                overflow-y: auto;
+                text-align: center;
+                width: 100%;
+            }
+
+            .warning-icon{
+                font-size: 2rem;
+                padding: 0 1rem;
+            }
+
+            .clipboard-btn{
+                margin-top: 1.5rem;
+                margin-bottom: 1.5rem;
+            }
+
+            .warning-container{
+                display: flex;
+                max-width: 400px;
+                text-align: left;
+                margin: auto;
+            }
+
+            .clipboard-data h4{
+                margin-top: 2rem;
+            }
+
+            .clipboard-data p{
+                margin-bottom: 1rem;
+            }
+
+            .clipboard-list{
+                padding: 1rem 0;
+                border-top: 1px solid #eee;
+            }
+
+
+            @media only screen
+            and (orientation: portrait)
+            and (max-device-width: 765px) {
+                .clipboard-container p, .clipboard-container h3{
+                    text-align: center;
+                }
+                .warning-container{
+                    flex-direction: column;
+                    align-items: center;
+                }
+                .warning-icon{
+                    margin-bottom: 1rem;
+                }
+            }
         `;
     }
 
     render() {
-        return html` 
-            
-            HALLLOOOO
-        `;
+        const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
+
+        if (this.clipboardSource) {
+
+            return html`
+                <link rel="stylesheet" href="${tabulatorCss}">
+                <div class="block clipboard-container">
+                    <div class="wrapper ${classMap({"table": this.clipboardFiles.files.length !== 0})}">
+                        <div class="inner">
+                            <h3>${i18n.t('file-source.clipboard-title')}</h3>
+                            <p>${i18n.t('file-source.clipboard-body')}<br><br></p>
+                            <p class="${classMap({"hidden": this.clipboardFiles.files.length !== 0})}">
+                                ${i18n.t('file-source.clipboard-no-files')}</p>
+                            <div class="clipboard-table ${classMap({"hidden": this.clipboardFiles.files.length === 0})}">
+                                <div id="select-all-wrapper">
+                                    <button class="button ${classMap({"hidden": !this.showSelectAllButton})}"
+                                            title="${i18n.t('nextcloud-file-picker.select-all-title')}"
+                                            @click="${() => {
+                                                this.selectAll();
+                                            }}">
+                                        ${i18n.t('nextcloud-file-picker.select-all')}
+                                    </button>
+                                    <button class="button ${classMap({"hidden": this.showSelectAllButton})}"
+                                            title="${i18n.t('nextcloud-file-picker.select-nothing-title')}"
+                                            @click="${() => {
+                                                this.deselectAll();
+                                            }}">
+                                        ${i18n.t('nextcloud-file-picker.select-nothing')}
+                                    </button>
+                                </div>
+                                <table id="clipboard-content-table" class="force-no-select"></table>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="clipboard-footer  ${classMap({"hidden": this.clipboardFiles.files.length === 0})}">
+                        <button class="button select-button is-primary" ?disabled="${this.clipboardSelectBtnDisabled}"
+                                @click="${() => {
+                                    this.sendClipboardFiles(this.tabulatorTable.getSelectedData());
+                                }}">${i18n.t('nextcloud-file-picker.select-files')}
+                        </button>
+                    </div>
+                </div>
+            `;
+
+        } else {
+            return html`                
+                <div class="block clipboard-container ${classMap({"table": this.clipboardFiles && this.clipboardFiles.files.length !== 0})}">
+                    <div class="inner">
+                        <h3>${i18n.t('file-sink.save-to-clipboard-title')}</h3>
+                        <p>${i18n.t('file-sink.save-to-clipboard-text')}</p>
+                        <button class="button is-primary clipboard-btn"
+                                ?disabled="${this.disabled}"
+                                @click="${() => { this.saveFilesToClipboard(); }}">
+                            ${this.buttonLabel || i18n.t('file-sink.save-to-clipboard-btn', {count:this.clipboardFiles.files.length})}
+                        </button>
+                        <div class="warning-container">
+                            <dbp-icon name="warning" class="warning-icon"></dbp-icon>
+                            <p>${i18n.t('file-sink.save-to-clipboard-warning')}</p>
+                        </div>
+                        
+                        <!-- filesink for clipboard TODO übersetzen-->
+
+                        <div class="${classMap({"hidden": this.clipboardFiles.files.length === 0})}">
+                        <button id="clipboard-download-button"
+                                    class="button is-right clipboard-btn"
+                                    @click="${this.openClipboardFileSink}"
+                                    >Aktuellen Zwischenablageninhalt speichern</button>
+                        </div>
+                        
+                        <dbp-file-sink id="file-sink-clipboard"
+                        context="${i18n.t('qualified-pdf-upload.save-field-label', {count: this.clipboardFiles ? this.clipboardFiles.files.length : 0})}"
+                        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>
+                        
+                        
+                        <div class="clipboard-data ${classMap({"hidden": this.clipboardFiles.files.length === 0})}">
+                            <h4>${i18n.t('file-sink.clipboard-files')}</h4>
+                            <p>${i18n.t('file-sink.clipboard-files-overwrite')}</p>
+                            ${this.getClipboardFileList()}
+                        </div>
+                    </div>
+                </div>
+            `;
+        }
     }
 }
diff --git a/packages/file-handling/src/file-sink.js b/packages/file-handling/src/file-sink.js
index b19b48ed..c433d6e3 100644
--- a/packages/file-handling/src/file-sink.js
+++ b/packages/file-handling/src/file-sink.js
@@ -13,6 +13,7 @@ import * as fileHandlingStyles from './styles';
 import { send } from '@dbp-toolkit/common/notification';
 import {humanFileSize} from '@dbp-toolkit/common/i18next';
 import * as utils from "../../../../../src/utils";
+import {FileHandlingClipboard} from "./dbp-file-handling-clipboard";
 
 
 /**
@@ -39,7 +40,6 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
         this.showClipboard = false;
 
         this.initialFileHandlingState = {target: '', path: ''};
-        this.clipBoardFiles = {files: ''};
     }
 
     static get scopedElements() {
@@ -47,6 +47,7 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
             'dbp-icon': Icon,
             'dbp-mini-spinner': MiniSpinner,
             'dbp-nextcloud-file-picker': NextcloudFilePicker,
+            'dbp-clipboard': FileHandlingClipboard,
         };
     }
 
@@ -74,7 +75,6 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
             showClipboard: { type: Boolean, attribute: 'show-clipboard' },
 
             initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
-            clipBoardFiles: {type: Object, attribute: 'clipboard-files'},
 
         };
     }
@@ -220,46 +220,54 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
         }
     }
 
-    saveFilesToClipboard()
-    {
-        //save it
-        let data = {};
-        if (this.files.length !== 0) {
-            data = {"files": this.files};
-            this.sendSetPropertyEvent('clipboard-files', data);
-            this.closeDialog();
-            send({
-                "summary": i18n.t('file-sink.save-to-clipboard-title'),
-                "body": i18n.t('file-sink.save-to-clipboard-body', {count: this.files.length}),
-                "type": "success",
-                "timeout": 5,
-            });
-            console.log("--------------", this.clipBoardFiles);
-        }
-
+    closeDialog(e) {
+        this.sendDestination();
+        MicroModal.close(this._('#modal-picker'));
     }
 
-    getClipboardFiles() {
-        let files = [];
-        for(let i = 0; i < this.clipBoardFiles.files.length; i ++)
-        {
-            files[i] =  html`<div class="clipboard-list"><strong>${this.clipBoardFiles.files[i].name}</strong> ${humanFileSize(this.clipBoardFiles.files[i].size)}</div>`;
+    getClipboardHtml() {
+        if (this.enabledTargets.includes('clipboard') && this.showClipboard) {
+            return html`
+                <dbp-clipboard 
+                   id="clipboard-file-sink"
+                   subscribe="clipboard-files:clipboard-files"
+                   lang="${this.lang}"
+                   auth-url="${this.nextcloudAuthUrl}"
+                   allowed-mime-types="${this.allowedMimeTypes}"
+                   @dbp-clipboard-file-picker-file-downloaded="${(event) => {
+                this.sendFileEvent(event.detail.file);}}">
+                </dbp-clipboard>`;
         }
-        return files;
+        return html``;
     }
 
-    closeDialog(e) {
-        this.sendDestination();
-        MicroModal.close(this._('#modal-picker'));
+    getNextcloudHtml() {
+        if (this.enabledTargets.includes('nextcloud') && this.nextcloudWebDavUrl !== "" && this.nextcloudAuthUrl !== "") {
+            return html`
+                <dbp-nextcloud-file-picker id="nextcloud-file-picker"
+                   class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}"
+                   directories-only
+                   max-selected-items="1"
+                   select-button-text="${i18n.t('file-sink.select-directory')}"
+                   ?disabled="${this.disabled}"
+                   lang="${this.lang}"
+                   auth-url="${this.nextcloudAuthUrl}"
+                   web-dav-url="${this.nextcloudWebDavUrl}"
+                   nextcloud-name="${this.nextcloudName}"
+                   directory-path="${this.nextcloudPath}"
+                   nextcloud-file-url="${this.nextcloudFileURL}"
+                   @dbp-nextcloud-file-picker-file-uploaded="${(event) => {
+                       this.uploadToNextcloud(event.detail);
+                   }}"
+                   @dbp-nextcloud-file-picker-file-uploaded-finished="${(event) => {
+                       this.finishedFileUpload(event);
+                   }}">
+                </dbp-nextcloud-file-picker>`;
+        }
+        return html``;
     }
 
-    /**
-     * Open Filesink for multiple files
-     */
-    async openClipboardFileSink() {
-        this._("#file-sink-clipboard").files = this.clipBoardFiles.files;
-        this._("#file-sink-clipboard").setAttribute("dialog-open", "");
-    }
+
 
     static get styles() {
         // language=css
@@ -283,67 +291,9 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
                 margin-bottom: 10px;
             }
             
-            .clipboard-container{
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-                padding: var(--FUPadding, 20px);
-            }
-            
-            .clipboard-container.table{
-                justify-content: start;
-            }
-            
-            .clipboard-container .inner{
-                overflow-y: auto;
-                text-align: center;
+            #clipboard-file-sink{
                 width: 100%;
-            }
-            
-            .warning-icon{
-                font-size: 2rem;
-                padding: 0 1rem;
-            }
-            
-            .clipboard-btn{
-                margin-top: 1.5rem;
-                margin-bottom: 1.5rem;
-            }
-            
-            .warning-container{
-                display: flex;
-                max-width: 400px;
-                text-align: left;
-                margin: auto;
-            }
-
-            .clipboard-data h4{
-                margin-top: 2rem;
-            }
-            
-            .clipboard-data p{
-                margin-bottom: 1rem;
-            }
-            
-            .clipboard-list{
-                padding: 1rem 0;
-                border-top: 1px solid #eee;
-            }
-            
-
-            @media only screen
-            and (orientation: portrait)
-            and (max-device-width: 765px) {
-                .clipboard-container p, .clipboard-container h3{
-                    text-align: center;
-                }
-                .warning-container{
-                    flex-direction: column;
-                    align-items: center;
-                }
-                .warning-icon{
-                    margin-bottom: 1rem;
-                }
+                height: 100%;
             }
         `;
     }
@@ -399,69 +349,10 @@ export class FileSink extends ScopedElementsMixin(DBPLitElement) {
                                 </div>
                             </div>
                             <div class="source-main ${classMap({"hidden": this.activeTarget !== "nextcloud" || this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}">
-                                <dbp-nextcloud-file-picker id="nextcloud-file-picker"
-                                                           class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}"
-                                                           directories-only
-                                                           max-selected-items="1"
-                                                           select-button-text="${i18n.t('file-sink.select-directory')}"
-                                                           ?disabled="${this.disabled}"
-                                                           lang="${this.lang}"
-                                                           auth-url="${this.nextcloudAuthUrl}"
-                                                           web-dav-url="${this.nextcloudWebDavUrl}"
-                                                           nextcloud-name="${this.nextcloudName}"
-                                                           directory-path="${this.nextcloudPath}"
-                                                           nextcloud-file-url="${this.nextcloudFileURL}"
-                                                           @dbp-nextcloud-file-picker-file-uploaded="${(event) => {
-                                                               this.uploadToNextcloud(event.detail);
-                                                           }}"
-                                                           @dbp-nextcloud-file-picker-file-uploaded-finished="${(event) => {
-                                                                this.finishedFileUpload(event);
-                                                            }}"></dbp-nextcloud-file-picker>
+                                ${this.getNextcloudHtml()}
                             </div>
                             <div class="source-main ${classMap({"hidden": this.activeTarget !== "clipboard" || isClipboardHidden})}">
-                                <div class="block clipboard-container ${classMap({"table": this.clipBoardFiles && this.clipBoardFiles.files.length !== 0})}">
-                                    <div class="inner">
-                                        <h3>${i18n.t('file-sink.save-to-clipboard-title')}</h3>
-                                        <p>${i18n.t('file-sink.save-to-clipboard-text')}</p>
-                                        <button class="button is-primary clipboard-btn"
-                                                ?disabled="${this.disabled}"
-                                                @click="${() => { this.saveFilesToClipboard(); }}">
-                                            ${this.buttonLabel || i18n.t('file-sink.save-to-clipboard-btn', {count:this.files.length})}
-                                        </button>
-                                        <div class="warning-container">
-                                            <dbp-icon name="warning" class="warning-icon"></dbp-icon>
-                                            <p>${i18n.t('file-sink.save-to-clipboard-warning')}</p>
-                                        </div>
-                                        
-                                        <!-- filesink for clipboard TODO übersetzen-->
-
-                                        <div clHALLLOOOOass="${classMap({"hidden": this.clipBoardFiles.files.length === 0})}">
-                                        <button id="clipboard-download-button"
-                                                    class="button is-right clipboard-btn"
-                                                    @click="${this.openClipboardFileSink}"
-                                                    >Aktuellen Zwischenablageninhalt speichern</button>
-                                        </div>
-                                        
-                                        <dbp-file-sink id="file-sink-clipboard"
-                                        context="${i18n.t('qualified-pdf-upload.save-field-label', {count: this.clipBoardFiles ? this.clipBoardFiles.files.length : 0})}"
-                                        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>
-                                        
-                                        
-                                        <div class="clipboard-data ${classMap({"hidden": this.clipBoardFiles.files.length === 0})}">
-                                            <h4>${i18n.t('file-sink.clipboard-files')}</h4>
-                                            <p>${i18n.t('file-sink.clipboard-files-overwrite')}</p>
-                                            ${this.getClipboardFiles()}
-                                        </div>
-                                    </div>
-                                </div>
+                                ${this.getClipboardHtml()}
                             </div>
                         </main>
                     </div>
diff --git a/packages/file-handling/src/file-source.js b/packages/file-handling/src/file-source.js
index 5d2fd118..04ed7eeb 100644
--- a/packages/file-handling/src/file-source.js
+++ b/packages/file-handling/src/file-source.js
@@ -10,9 +10,8 @@ import {NextcloudFilePicker} from "./dbp-nextcloud-file-picker";
 import {classMap} from 'lit-html/directives/class-map.js';
 import MicroModal from './micromodal.es';
 import * as fileHandlingStyles from './styles';
-import Tabulator from "tabulator-tables";
-import {humanFileSize} from "@dbp-toolkit/common/i18next";
 import {name as pkgName} from "../package.json";
+import {FileHandlingClipboard} from "./dbp-file-handling-clipboard";
 
 function mimeTypesToAccept(mimeTypes) {
     // Some operating systems can't handle mime types and
@@ -56,16 +55,10 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
         this.activeTarget = 'local';
         this.isDialogOpen = false;
         this.firstOpen = true;
-        this.tabulatorTable = null;
-        this.maxSelectedItems = true;
-        this.showSelectAllButton = true;
-        this.clipboardSelectBtnDisabled = true;
         this.showClipboard = false;
 
         this.initialFileHandlingState = {target: '', path: ''};
-        this.clipboardFiles = {files: ''};
 
-        this._onReceiveBeforeUnload = this.onReceiveBeforeUnload.bind(this);
 
     }
 
@@ -74,6 +67,7 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
             'dbp-icon': Icon,
             'dbp-mini-spinner': MiniSpinner,
             'dbp-nextcloud-file-picker': NextcloudFilePicker,
+            'dbp-clipboard': FileHandlingClipboard,
         };
     }
 
@@ -97,13 +91,9 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
             decompressZip: { type: Boolean, attribute: 'decompress-zip' },
             activeTarget: { type: String, attribute: 'active-target' },
             isDialogOpen: { type: Boolean, attribute: 'dialog-open' },
-            showSelectAllButton: { type: Boolean, attribute: true },
-            clipboardSelectBtnDisabled: { type: Boolean, attribute: true },
             showClipboard: { type: Boolean, attribute: 'show-clipboard' },
 
             initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
-            clipboardFiles: {type: Object, attribute: 'clipboard-files'},
-
         };
     }
 
@@ -133,10 +123,6 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
                         this.nextcloudPath = this.initialFileHandlingState.path;
                     }
                   break;
-                case "clipboardFiles":
-                    this.generateClipboardTable();
-                    break;
-
             }
         });
         super.update(changedProperties);
@@ -145,7 +131,6 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
 
     connectedCallback() {
         super.connectedCallback();
-        const that = this;
         this.updateComplete.then(() => {
             this.dropArea = this._('#dropArea');
             ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
@@ -159,109 +144,16 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
             });
             this.dropArea.addEventListener('drop', this.handleDrop.bind(this), false);
             this._('#fileElem').addEventListener('change', this.handleChange.bind(this));
-
-            // see: http://tabulator.info/docs/4.7
-            this.tabulatorTable = new Tabulator(this._("#clipboard-content-table"), {
-                layout: "fitColumns",
-                selectable: this.maxSelectedItems,
-                selectableRangeMode: "drag",
-                responsiveLayout: true,
-                placeholder: i18n.t('nextcloud-file-picker.no-data-type'),
-                resizableColumns:false,
-                columns: [
-                    {title: "", field: "type", align:"center", headerSort:false, width:50, responsive:1, formatter: (cell, formatterParams, onRendered) => {
-                            const icon_tag =  that.constructor.getScopedTagName("dbp-icon");
-                            let icon = `<${icon_tag} name="empty-file" class="nextcloud-picker-icon"></${icon_tag}>`;
-                            return icon;
-                        }},
-                    {title: i18n.t('nextcloud-file-picker.filename'), responsive: 0, widthGrow:5,  minWidth: 150, field: "name", sorter: "alphanum",
-                        formatter: (cell) => {
-                            let data = cell.getRow().getData();
-                            if (data.edit) {
-                                cell.getElement().classList.add("fokus-edit");
-                            }
-                            return cell.getValue();
-                        }},
-                    {title: i18n.t('nextcloud-file-picker.size'), responsive: 4, widthGrow:1, minWidth: 50, field: "size", formatter: (cell, formatterParams, onRendered) => {
-                            return cell.getRow().getData().type === "directory" ? "" : humanFileSize(cell.getValue());
-                        }},
-                    {title: i18n.t('nextcloud-file-picker.mime-type'), responsive: 2, widthGrow:1, minWidth: 20, field: "type", formatter: (cell, formatterParams, onRendered) => {
-                            if (typeof cell.getValue() === 'undefined') {
-                                return "";
-                            }
-                            const [, fileSubType] = cell.getValue().split('/');
-                            return fileSubType;
-                        }},
-                    {title: i18n.t('nextcloud-file-picker.last-modified'), responsive: 3, widthGrow:1, minWidth: 150, field: "lastModified",sorter: (a, b, aRow, bRow, column, dir, sorterParams) => {
-                            const a_timestamp = Date.parse(a);
-                            const b_timestamp = Date.parse(b);
-                            return a_timestamp - b_timestamp;
-                        }, formatter:function(cell, formatterParams, onRendered) {
-                            const timestamp = new Date(cell.getValue());
-                            const year = timestamp.getFullYear();
-                            const month = ("0" + (timestamp.getMonth() + 1)).slice(-2);
-                            const date = ("0" + timestamp.getDate()).slice(-2);
-                            const hours = ("0" + timestamp.getHours()).slice(-2);
-                            const minutes = ("0" + timestamp.getMinutes()).slice(-2);
-                            return date + "." + month + "." + year + " " + hours + ":" + minutes;
-                        }},
-                    {title: "file", field: "file", visible: false}
-                ],
-                initialSort:[
-                    {column:"name", dir:"asc"},
-                    {column:"type", dir:"asc"},
-                ],
-                rowClick: (e, row) => {
-                    if (this.tabulatorTable !== null
-                        && this.tabulatorTable.getSelectedRows().length === this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())).length) {
-                        this.showSelectAllButton = false;
-                    } else {
-                        this.showSelectAllButton = true;
-                    }
-                },
-                rowSelectionChanged: (data, rows) => {
-                    if (this.tabulatorTable && this.tabulatorTable.getSelectedRows().length > 0) {
-                        this.clipboardSelectBtnDisabled = false;
-                    } else {
-                        this.clipboardSelectBtnDisabled = true;
-                    }
-                }
-            });
         });
 
-        window.addEventListener('beforeunload', this._onReceiveBeforeUnload);
     }
 
     disconnectedCallback() {
-        // remove event listeners
-
-        //We doesn't want to deregister this event, because we want to use this event over activities
-        //window.removeEventListener('beforeunload', this._onReceiveBeforeUnload);
 
         super.disconnectedCallback();
     }
 
-    /**
-     * Select all files from tabulator table
-     *
-     */
-    selectAll() {
-        this.tabulatorTable.selectRow(this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())));
-        if (this.tabulatorTable.getSelectedRows().length > 0) {
-            this.showSelectAllButton = false;
-            console.log("Show Select All Button:", this.showSelectAllButton);
-        }
-    }
 
-    /**
-     * Deselect files from tabulator table
-     *
-     */
-    deselectAll() {
-        this.tabulatorTable.deselectRow();
-        this.showSelectAllButton = true;
-        console.log("Show Select All Button:", this.showSelectAllButton);
-    }
 
     preventDefaults (e) {
         e.preventDefault();
@@ -487,8 +379,8 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
         }
 
         if (this.enabledTargets.includes('clipboard')) {
-            this.generateClipboardTable();
-            this.showSelectAllButton = true;
+            this._("#clipboard-file-picker").generateClipboardTable();
+            this._("#clipboard-file-picker").showSelectAllButton = true;
         }
 
         const filePicker = this._('#modal-picker');
@@ -521,74 +413,45 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
         MicroModal.close(this._('#modal-picker'));
     }
 
-
-
-    generateClipboardTable() {
-        let data = [];
-        for (let i = 0; i < this.clipboardFiles.files.length; i++){
-            data[i] = {
-                name: this.clipboardFiles.files[i].name,
-                size: this.clipboardFiles.files[i].size,
-                type: this.clipboardFiles.files[i].type,
-                lastModified: this.clipboardFiles.files[i].lastModified,
-                file: this.clipboardFiles.files[i]
-            };
-        }
-
-        if (this.tabulatorTable !== null){
-            this.tabulatorTable.clearData();
-            this.tabulatorTable.setData(data);
+    getClipboardHtml() {
+        if (this.enabledTargets.includes('clipboard') && this.showClipboard) {
+            return html`
+                <dbp-clipboard 
+                   id="clipboard-file-picker"
+                   clipboard-source
+                   subscribe="clipboard-files:clipboard-files"
+                   lang="${this.lang}"
+                   auth-url="${this.nextcloudAuthUrl}"
+                   allowed-mime-types="${this.allowedMimeTypes}"
+                   @dbp-clipboard-file-picker-file-downloaded="${(event) => {
+                    this.sendFileEvent(event.detail.file);}}">
+                </dbp-clipboard>`;
         }
+        return html``;
     }
 
-
-    async sendClipboardFiles(files) {
-
-        for(let i = 0; i < files.length; i ++)
-        {
-            await this.sendFileEvent(files[i].file);
+    getNextcloudHtml() {
+        if (this.enabledTargets.includes('nextcloud') && this.nextcloudWebDavUrl !== "" && this.nextcloudAuthUrl !== "") {
+            return html`
+                <dbp-nextcloud-file-picker id="nextcloud-file-picker"
+                   class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}"
+                   ?disabled="${this.disabled}"
+                   lang="${this.lang}"
+                   auth-url="${this.nextcloudAuthUrl}"
+                   web-dav-url="${this.nextcloudWebDavUrl}"
+                   nextcloud-name="${this.nextcloudName}"
+                   nextcloud-file-url="${this.nextcloudFileURL}"
+                   allowed-mime-types="${this.allowedMimeTypes}"
+                   @dbp-nextcloud-file-picker-file-downloaded="${(event) => {
+                    this.sendFileEvent(event.detail.file);}}">
+                </dbp-nextcloud-file-picker>`;
         }
-        this.tabulatorTable.deselectRow();
-        this.closeDialog();
-
+        return html``;
     }
 
-    /**
-     * 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.showClipboard || !this.hasEnabledSource("clipboard") || this.clipboardFiles.files.length === 0) {
-            return;
-       }
-
-        // we need to handle custom events ourselves
-       if(event.target && event.target.activeElement && event.target.activeElement.nodeName) {
-
-            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('page-leaving-warn-dialogue'));
-                // 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 = '';
-        }
 
 
 
-    }
-
     static get styles() {
         // language=css
         return css`
@@ -625,46 +488,12 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
             #dropArea.highlight {
                 border-color: var(--FUBorderColorHighlight, purple);
             }
-
-            .clipboard-container{
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-                padding: var(--FUPadding, 20px);
-                width: 100%;
-                height: 100%;
-                position: relative;
-            }
-
-            .clipboard-container .wrapper{
-                overflow-y: auto;
-                text-align: center;
-                width: 100%;
-                height: 100%;
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-            }
-
-            .clipboard-container .wrapper.table{
-                justify-content: start;
-            }
             
-            .clipboard-container .wrapper .inner{
-                overflow-y: auto;
-                text-align: center;
+            #clipboard-file-picker{
                 width: 100%;
+                height: 100%;
             }
             
-            .clipboard-footer{
-                align-self: end;
-            }
-            
-            #select-all-wrapper{
-                text-align: right;
-            }
-            
-            
              @media only screen
             and (orientation: portrait)
             and (max-device-width: 800px) {
@@ -673,7 +502,6 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
                 }
             
             }
-            
         `;
     }
 
@@ -685,7 +513,6 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
             allowedMimeTypes += ",application/zip,application/x-zip-compressed";
         }
 
-        const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
 
         return html`
 <!--
@@ -694,7 +521,6 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
                 @click="${() => { this.openDialog(); }}">${i18n.t('file-source.open-menu')}</button>
 -->
             <div class="modal micromodal-slide" id="modal-picker" aria-hidden="true">
-                <link rel="stylesheet" href="${tabulatorCss}">
                 <div class="modal-overlay" tabindex="-1" data-micromodal-close>
                     <div class="modal-container" role="dialog" aria-modal="true" aria-labelledby="modal-picker-title">
                         <nav class="modal-nav">
@@ -744,48 +570,10 @@ export class FileSource extends ScopedElementsMixin(DBPLitElement) {
                                 </div>
                             </div>
                             <div class="source-main ${classMap({"hidden": this.activeTarget !== "nextcloud" || this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}">
-                                <dbp-nextcloud-file-picker id="nextcloud-file-picker"
-                                       class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}"
-                                       ?disabled="${this.disabled}"
-                                       lang="${this.lang}"
-                                       auth-url="${this.nextcloudAuthUrl}"
-                                       web-dav-url="${this.nextcloudWebDavUrl}"
-                                       nextcloud-name="${this.nextcloudName}"
-                                       nextcloud-file-url="${this.nextcloudFileURL}"
-                                       allowed-mime-types="${this.allowedMimeTypes}"
-                                       @dbp-nextcloud-file-picker-file-downloaded="${(event) => {
-                                    this.sendFileEvent(event.detail.file);
-                                }}"></dbp-nextcloud-file-picker>
+                                ${this.getNextcloudHtml()}
                             </div>
                             <div class="source-main ${classMap({"hidden": (this.activeTarget !== "clipboard" || isClipboardHidden)})}">
-                                <div class="block clipboard-container">
-                                    <div class="wrapper ${classMap({"table": this.clipboardFiles.files.length !== 0})}">
-                                        <div class="inner">
-                                            <h3>${i18n.t('file-source.clipboard-title')}</h3>
-                                            <p>${i18n.t('file-source.clipboard-body')}<br><br></p>
-                                            <p class="${classMap({"hidden": this.clipboardFiles.files.length !== 0})}">${i18n.t('file-source.clipboard-no-files')}</p>
-                                            <div class="clipboard-table ${classMap({"hidden": this.clipboardFiles.files.length === 0})}">
-                                                <div id="select-all-wrapper">
-                                                    <button class="button ${classMap({"hidden": !this.showSelectAllButton})}"
-                                                            title="${i18n.t('nextcloud-file-picker.select-all-title')}"
-                                                            @click="${() => { this.selectAll(); }}">
-                                                            ${i18n.t('nextcloud-file-picker.select-all')}
-                                                    </button>
-                                                    <button class="button ${classMap({"hidden": this.showSelectAllButton})}"
-                                                            title="${i18n.t('nextcloud-file-picker.select-nothing-title')}"
-                                                            @click="${() => { this.deselectAll(); }}">
-                                                            ${i18n.t('nextcloud-file-picker.select-nothing')}
-                                                    </button>
-                                                </div>
-                                                <table id="clipboard-content-table" class="force-no-select"></table>
-                                            </div>
-                                        </div>
-                                    </div>
-                                    <div class="clipboard-footer  ${classMap({"hidden": this.clipboardFiles.files.length === 0 })}">
-                                        <button class="button select-button is-primary" ?disabled="${ this.clipboardSelectBtnDisabled }"
-                                                @click="${() => { this.sendClipboardFiles(this.tabulatorTable.getSelectedData()); }}">${i18n.t('nextcloud-file-picker.select-files')}</button>
-                                    </div>
-                                </div>
+                                ${this.getClipboardHtml()}
                             </div>
                         </main>
                     </div>
diff --git a/packages/file-handling/src/i18n/de/translation.json b/packages/file-handling/src/i18n/de/translation.json
index b59f1ecb..f6a46d42 100644
--- a/packages/file-handling/src/i18n/de/translation.json
+++ b/packages/file-handling/src/i18n/de/translation.json
@@ -96,5 +96,10 @@
     "select-nothing-title": "Alle gewählten Dateien nicht mehr selektieren",
     "abort": "Vorgang abbrechen",
     "abort-message": "Vorgang wurde abgebrochen."
+  },
+  "clipboard": {
+    "file-warning": "Achtung!",
+    "file-warning-body": "Es befindet sich noch eine Datei in der Zwischenablage. Die Zwischenablage wird beim Verlassen der Seite automatisch verworfen.",
+    "file-warning-body_plural": "Es befinden sich noch {{count}} Dateien in der Zwischenablage. Die Zwischenablage wird beim Verlassen der Seite automatisch verworfen."
   }
 }
diff --git a/packages/file-handling/src/i18n/en/translation.json b/packages/file-handling/src/i18n/en/translation.json
index b2d0a2a8..881fb18d 100644
--- a/packages/file-handling/src/i18n/en/translation.json
+++ b/packages/file-handling/src/i18n/en/translation.json
@@ -96,5 +96,10 @@
     "select-nothing-title": "Select no files",
     "abort": "Cancel process",
     "abort-message": "The process was canceled."
+  },
+  "clipboard": {
+    "file-warning": "Attention!",
+    "file-warning-body": "There is still a file on the clipboard. The clipboard is automatically discarded when you exit the page.",
+    "file-warning-body_plural": "There are still {{count}} files on the clipboard. The clipboard is automatically discarded when you exit the page."
   }
 }
-- 
GitLab