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 = 5;
this.selectAllButton = true;
this.initialFileHandlingState = {target: '', path: ''};
this.clipBoardFiles = {files: ''};
Steinwender, Tamara
committed
this.selectedClipBoardFiles = {files: []};
}
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' },
firstOpen: { type: Boolean, attribute: false },
nextcloudPath: { type: String, attribute: false },
Steinwender, Tamara
committed
maxSelectedItems: { type: Number, attribute: 'max-selected-items' },
selectAllButton: { type: Boolean, attribute: false },
initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
clipBoardFiles: {type: Object, attribute: 'clipboard-files'},
Steinwender, Tamara
committed
selectedClipBoardFiles: {type: Object, attribute: false},
}
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;
}
});
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// 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"},
],
rowFormatter: (row) => {
let data = row.getData();
if (!this.checkFileType(data)) {
row.getElement().classList.add("no-select");
row.getElement().classList.remove("tabulator-selectable");
}
},
rowSelectionChanged: (data, rows) => {
this.folderIsSelected = i18n.t('nextcloud-file-picker.load-in-folder');
},
selectableCheck:function(row){
//row - row component
return that.checkFileType(row.getData()); //allow selection of rows where the age is greater than 18
},
rowClick: (e, row) => {
/*const data = row.getData();
if (!row.getElement().classList.contains("no-select")) {
if (this.tabulatorTable.getSelectedRows().length === this.tabulatorTable.getRows().filter(row => row.getData().type != 'directory' && this.checkFileType(row.getData())).length) {
this.selectAllButton = false;
}
else {
this.selectAllButton = true;
}
}
else{
row.deselect();
}*/
}
});
Steinwender, Tamara
committed
/**
* Select all files from tabulator table
*
*/
selectAll() {
this.tabulatorTable.selectRow();
console.log(".....", this.tabulatorTable.getSelectedRows());
if (this.tabulatorTable.getSelectedRows().length > 0) {
this.selectAllButton = false;
}
}
/**
* Deselect files from tabulator table
*
*/
deselectAll() {
this.selectAllButton = true;
//this.tabulatorTable.getSelectedRows().forEach(row => row.deselect());
this.tabulatorTable.deselectRow();
}
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();
}
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
Steinwender, Tamara
committed
let data = [];
data[0] = {
name: "test",
size: 1234324432,
type: "application/pdf",
lastModified: 1616417323351,
file: null
};
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);
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();
}
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
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
.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;
}
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;
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") })}">
<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"})}">
<div class="block clipboard-container">
Steinwender, Tamara
committed
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
<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 ">
${this.getClipboardFiles()}
<div id="select-all-wrapper"">
<button class="button ${classMap({hidden: !this.selectAllButton})}"
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.selectAllButton})}"
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"
@click="${() => { this.sendClipboardFiles(this.tabulatorTable.getSelectedData()); }}">${i18n.t('nextcloud-file-picker.select-files')}</button>
</div>