Newer
Older
import {i18n} from './i18n';
import {css, html} from 'lit-element';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import * as commonUtils from "@dbp-toolkit/common/utils";
import {Icon, MiniSpinner} from '@dbp-toolkit/common';
import {send as notify} from '@dbp-toolkit/common/notification';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {NextcloudFilePicker} from "./dbp-nextcloud-file-picker";
import {classMap} from 'lit-html/directives/class-map.js';
import MicroModal from './micromodal.es';
Steinwender, Tamara
committed
import * as fileHandlingStyles from './styles';
Steinwender, Tamara
committed
import Tabulator from "tabulator-tables";
import {humanFileSize} from "@dbp-toolkit/common/i18next";
import {name as pkgName} from "../package.json";
function mimeTypesToAccept(mimeTypes) {
// Some operating systems can't handle mime types and
// need file extensions, this tries to add them for some..
let mapping = {
'application/pdf': ['.pdf'],
'application/zip': ['.zip'],
};
let accept = [];
mimeTypes.split(',').forEach((mime) => {
accept.push(mime);
if (mime.trim() in mapping) {
accept = accept.concat(mapping[mime.trim()]);
}
});
return accept.join(',');
}
* FileSource web component
export class FileSource extends ScopedElementsMixin(DBPLitElement) {
this.nextcloudName ='Nextcloud';
this.nextcloudFileURL = '';
this.allowedMimeTypes = '*/*';
this.enabledTargets = 'local';
this.decompressZip = false;
this.activeTarget = 'local';
this.isDialogOpen = false;
Steinwender, Tamara
committed
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);
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-mini-spinner': MiniSpinner,
'dbp-nextcloud-file-picker': NextcloudFilePicker,
};
}
/**
* See: https://lit-element.polymer-project.org/guide/properties#initialize
*/
static get properties() {
context: { type: String, attribute: 'context'},
allowedMimeTypes: { type: String, attribute: 'allowed-mime-types' },
enabledTargets: { type: String, attribute: 'enabled-targets' },
nextcloudAuthUrl: { type: String, attribute: 'nextcloud-auth-url' },
nextcloudWebDavUrl: { type: String, attribute: 'nextcloud-web-dav-url' },
nextcloudName: { type: String, attribute: 'nextcloud-name' },
nextcloudFileURL: { type: String, attribute: 'nextcloud-file-url' },
buttonLabel: { type: String, attribute: 'button-label' },
disabled: { type: Boolean },
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'},
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case "lang":
i18n.changeLanguage(this.lang);
case "enabledTargets":
if (!this.hasEnabledSource(this.activeTarget)) {
this.activeTarget = this.enabledTargets.split(",")[0];
case "isDialogOpen":
if (this.isDialogOpen) {
// this.setAttribute("dialog-open", "");
this.openDialog();
} else {
this.removeAttribute("dialog-open");
// this.closeDialog();
case "initialFileHandlingState":
//check if default destination is set
if (this.firstOpen) {
this.nextcloudPath = this.initialFileHandlingState.path;
}
break;
case "clipboardFiles":
this.generateClipboardTable();
break;
}
});
super.update(changedProperties);
}
connectedCallback() {
super.connectedCallback();
Steinwender, Tamara
committed
const that = this;
this.updateComplete.then(() => {
this.dropArea = this._('#dropArea');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.dropArea.addEventListener(eventName, this.preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
this.dropArea.addEventListener(eventName, this.highlight.bind(this), false);
});
['dragleave', 'drop'].forEach(eventName => {
this.dropArea.addEventListener(eventName, this.unhighlight.bind(this), false);
});
this.dropArea.addEventListener('drop', this.handleDrop.bind(this), false);
this._('#fileElem').addEventListener('change', this.handleChange.bind(this));
Steinwender, Tamara
committed
// 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());
}},
Steinwender, Tamara
committed
{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}
Steinwender, Tamara
committed
],
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;
Steinwender, Tamara
committed
}
},
rowSelectionChanged: (data, rows) => {
if (this.tabulatorTable && this.tabulatorTable.getSelectedRows().length > 0) {
this.clipboardSelectBtnDisabled = false;
} else {
this.clipboardSelectBtnDisabled = true;
Steinwender, Tamara
committed
}
}
});
window.addEventListener('beforeunload', this._onReceiveBeforeUnload);
}
disconnectedCallback() {
// remove event listeners
window.removeEventListener('beforeunload', this._onReceiveBeforeUnload);
super.disconnectedCallback();
Steinwender, Tamara
committed
/**
* Select all files from tabulator table
*
*/
selectAll() {
this.tabulatorTable.selectRow(this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())));
Steinwender, Tamara
committed
if (this.tabulatorTable.getSelectedRows().length > 0) {
this.showSelectAllButton = false;
console.log("Show Select All Button:", this.showSelectAllButton);
Steinwender, Tamara
committed
}
}
/**
* Deselect files from tabulator table
*
*/
deselectAll() {
this.tabulatorTable.deselectRow();
this.showSelectAllButton = true;
console.log("Show Select All Button:", this.showSelectAllButton);
Steinwender, Tamara
committed
}
preventDefaults (e) {
e.preventDefault();
e.stopPropagation();
}
highlight(e) {
this.dropArea.classList.add('highlight');
this.dropArea.classList.remove('highlight');
if (this.disabled) {
return;
}
let dt = e.dataTransfer;
let files = dt.files;
this.handleFiles(files);
}
async handleChange(e) {
let fileElem = this._('#fileElem');
if (fileElem.files.length === 0) {
return;
}
await this.handleFiles(fileElem.files);
// reset the element's value so the user can upload the same file(s) again
fileElem.value = '';
}
/**
* Handles files that were dropped to or selected in the component
*
* @param files
* @returns {Promise<void>}
*/
// console.log('handleFiles: files.length = ' + files.length);
// this.dispatchEvent(new CustomEvent("dbp-file-source-selection-start",
// { "detail": {}, bubbles: true, composed: true }));
await commonUtils.asyncArrayForEach(files, async (file) => {
console.log('file \'' + file.name + '\' has size=0 and is denied!');
// check if we want to decompress the zip and queue the contained files
if (this.decompressZip
&& (file.type === "application/zip" || file.type === "application/x-zip-compressed")) {
// add decompressed files to tempFilesToHandle
await commonUtils.asyncArrayForEach(
await this.decompressZIP(file), (file) => this.sendFileEvent(file));
return;
} else if (this.allowedMimeTypes && !this.checkFileType(file)) {
return;
await this.sendFileEvent(file);
});
// this.dispatchEvent(new CustomEvent("dbp-file-source-selection-finished",
// { "detail": {}, bubbles: true, composed: true }));
this.closeDialog();
/**
* @param file
*/
sendFileEvent(file) {
Steinwender, Tamara
committed
MicroModal.close(this._('#modal-picker'));
const data = {"file": file};
const event = new CustomEvent("dbp-file-source-file-selected", { "detail": data, bubbles: true, composed: true });
this.dispatchEvent(event);
if (this.activeTarget == 'nextcloud') {
data = {"target": this.activeTarget, "path": this._("#nextcloud-file-picker").directoryPath};
data = {"target": this.activeTarget};
this.sendSetPropertyEvent('initial-file-handling-state', data);
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;
}
return this.enabledTargets.split(',').includes(source);
/**
* Decompress files synchronously
*
* @param file
* @returns {Promise<Array>}
*/
async decompressZIP(file) {
// see: https://stuk.github.io/jszip/
let JSZip = (await import('jszip/dist/jszip.js')).default;
let filesToHandle = [];
// load zip file
await JSZip.loadAsync(file)
.then(async (zip) => {
// we are not using zip.forEach because we need to handle those files synchronously which
// isn't supported by JSZip (see https://github.com/Stuk/jszip/issues/281)
// using zip.files directly works great!
await commonUtils.asyncObjectForEach(zip.files, async (zipEntry) => {
// skip directory entries
if (zipEntry.dir) {
return;
}
await zipEntry.async("blob")
Bekerle, Patrizio
committed
.then(async (blob) => {
// get mime type of Blob, see https://github.com/Stuk/jszip/issues/626
const mimeType = await commonUtils.getMimeTypeOfFile(blob);
// create new file with name and mime type
const zipEntryFile = new File([blob], zipEntry.name, { type: mimeType });
// check mime type
if (!this.checkFileType(zipEntryFile)) {
return;
}
filesToHandle.push(zipEntryFile);
}, (e) => {
// handle the error
console.error("Decompressing of file in " + file.name + " failed: " + e.message);
});
});
}, function (e) {
// handle the error
Bekerle, Patrizio
committed
console.error("Loading of " + file.name + " failed: " + e.message);
// no suitable files found
if (filesToHandle.length === 0) {
console.error('ZIP file does not contain any files of ' + this.allowedMimeTypes);
//throw new Error('ZIP file does not contain any files of ' + this.allowedMimeTypes);
notify({
"summary": i18n.t('file-source.no-usable-files-in-zip'),
"body": i18n.t('file-source.no-usable-files-hint') + this.allowedMimeTypes,
"type": 'danger',
"timeout": 0,
});
return filesToHandle;
}
async sendFinishedEvent(response, file, sendFile = false) {
if (response === undefined) {
return;
}
let data = {
fileName: file.name,
status: response.status,
json: {"hydra:description": ""}
};
try {
await response.json().then((json) => {
data.json = json;
});
} catch (e) {
//
}
if (sendFile) {
data.file = file;
}
const event = new CustomEvent("dbp-file-source-file-finished", { "detail": data, bubbles: true, composed: true });
const filePicker = this._('#nextcloud-file-picker');
// check if element is already in the dom (for example if "dialog-open" attribute is set)
if (filePicker && filePicker.webDavClient !== null) {
filePicker.loadDirectory(filePicker.directoryPath);
if (this.enabledTargets.includes('nextcloud')) {
this.loadWebdavDirectory();
}
if (this.enabledTargets.includes('clipboard')) {
this.generateClipboardTable();
}
const filePicker = this._('#modal-picker');
// check if element is already^ in the dom (for example if "dialog-open" attribute is set)
if (filePicker) {
MicroModal.show(filePicker, {
disableScroll: true,
onClose: modal => { this.isDialogOpen = false;
this._('#nextcloud-file-picker').selectAllButton = true;}
});
}
//check if default source is set
if (this.initialFileHandlingState.target !== '' && typeof this.initialFileHandlingState.target !== 'undefined' && this.firstOpen) {
this.activeDestination = this.initialFileHandlingState.target;
this.nextcloudPath = this.initialFileHandlingState.path;
if (this._('#nextcloud-file-picker').webDavClient !== null) {
this._('#nextcloud-file-picker').loadDirectory(this.initialFileHandlingState.path);
//console.log("load default nextcloud source", this.initialFileHandlingState.target);
closeDialog() {
this._('#nextcloud-file-picker').selectAllButton = true;
Steinwender, Tamara
committed
MicroModal.close(this._('#modal-picker'));
}
Steinwender, Tamara
committed
generateClipboardTable() {
Steinwender, Tamara
committed
let data = [];
for (let i = 0; i < this.clipboardFiles.files.length; i++){
Steinwender, Tamara
committed
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]
Steinwender, Tamara
committed
};
}
if (this.tabulatorTable !== null){
this.tabulatorTable.clearData();
this.tabulatorTable.setData(data);
Steinwender, Tamara
committed
async sendClipboardFiles(files) {
for(let i = 0; i < files.length; i ++)
Steinwender, Tamara
committed
await this.sendFileEvent(files[i].file);
Steinwender, Tamara
committed
this.tabulatorTable.deselectRow();
this.closeDialog();
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
}
/**
* 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`
${commonStyles.getThemeCSS()}
Steinwender, Tamara
committed
${fileHandlingStyles.getFileHandlingCss()}
Steinwender, Tamara
committed
p {
margin-top: 0;
}
.block {
margin-bottom: 10px;
}
#dropArea {
border: var(--FUBorderWidth, 2px) var(--FUBorderStyle, dashed) var(--FUBBorderColor, black);
border-radius: var(--FUBorderRadius, 0);
margin: var(--FUMargin, 0px);
padding: var(--FUPadding, 20px);
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
Steinwender, Tamara
committed
#dropArea.highlight {
border-color: var(--FUBorderColorHighlight, purple);
}
Steinwender, Tamara
committed
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
.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 .inner{
overflow-y: auto;
text-align: center;
width: 100%;
}
.clipboard-footer{
align-self: end;
}
#select-all-wrapper{
text-align: right;
}
Steinwender, Tamara
committed
@media only screen
and (orientation: portrait)
Steinwender, Tamara
committed
and (max-device-width: 800px) {
Steinwender, Tamara
committed
Steinwender, Tamara
committed
let allowedMimeTypes = this.allowedMimeTypes;
const isClipboardHidden = !this.showClipboard;
if (this.decompressZip) {
allowedMimeTypes += ",application/zip,application/x-zip-compressed";
}
Steinwender, Tamara
committed
const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
<!--
<button class="button"
?disabled="${this.disabled}"
@click="${() => { this.openDialog(); }}">${i18n.t('file-source.open-menu')}</button>
-->
<div class="modal micromodal-slide" id="modal-picker" aria-hidden="true">
Steinwender, Tamara
committed
<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">
<div title="${i18n.t('file-source.nav-local')}"
@click="${() => { this.activeTarget = "local"; }}"
class="${classMap({"active": this.activeTarget === "local", hidden: !this.hasEnabledSource("local")})}">
<dbp-icon class="nav-icon" name="laptop"></dbp-icon>
<p>${i18n.t('file-source.nav-local')}</p>
</div>
<div title="Nextcloud"
@click="${() => { this.activeTarget = "nextcloud"; this.loadWebdavDirectory();}}"
class="${classMap({"active": this.activeTarget === "nextcloud", hidden: !this.hasEnabledSource("nextcloud") || this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}">
<dbp-icon class="nav-icon" name="cloud"></dbp-icon>
<p> ${this.nextcloudName} </p>
@click="${() => { this.activeTarget = "clipboard"; }}"
class="${classMap({"active": this.activeTarget === "clipboard", hidden: !this.hasEnabledSource("clipboard") || isClipboardHidden })}">
<dbp-icon class="nav-icon" name="clipboard"></dbp-icon>
<p>Clipboard</p>
</div>
</nav>
<div class="modal-header">
<button title="${i18n.t('file-source.modal-close')}" class="modal-close" aria-label="Close modal" @click="${() => {this.closeDialog();}}">
<dbp-icon name="close" class="close-icon"></dbp-icon>
<p class="modal-context"> ${this.context}</p>
</div>
<main class="modal-content" id="modal-picker-content">
<div class="source-main ${classMap({"hidden": this.activeTarget !== "local"})}">
<div id="dropArea">
<div class="block">
<p>${this.text || i18n.t('intro')}</p>
</div>
<input ?disabled="${this.disabled}"
type="file"
id="fileElem"
multiple
accept="${mimeTypesToAccept(allowedMimeTypes)}"
name='file'>
<label class="button is-primary" for="fileElem" ?disabled="${this.disabled}">
${this.buttonLabel || i18n.t('upload-label')}
</label>
</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>
<div class="source-main ${classMap({"hidden": (this.activeTarget !== "clipboard" || isClipboardHidden)})}">
<div class="block clipboard-container">
Steinwender, Tamara
committed
<div class="wrapper">
<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})}"
Steinwender, Tamara
committed
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})}"
Steinwender, Tamara
committed
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 }"
Steinwender, Tamara
committed
@click="${() => { this.sendClipboardFiles(this.tabulatorTable.getSelectedData()); }}">${i18n.t('nextcloud-file-picker.select-files')}</button>
</div>