-
Steinwender, Tamara authoredSteinwender, Tamara authored
file-source.js 30.39 KiB
import {createInstance} from './i18n';
import {css, html} from 'lit';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import * as commonUtils from '@dbp-toolkit/common/utils';
import {Icon, MiniSpinner} from '@dbp-toolkit/common';
import {send} from '@dbp-toolkit/common/notification';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {NextcloudFilePicker} from './nextcloud-file-picker';
import {classMap} from 'lit/directives/class-map.js';
import MicroModal from './micromodal.es';
import * as fileHandlingStyles from './styles';
import {Clipboard} from '@dbp-toolkit/file-handling/src/clipboard';
import DbpFileHandlingLitElement from './dbp-file-handling-lit-element';
import {humanFileSize} from '@dbp-toolkit/common/i18next';
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(DbpFileHandlingLitElement) {
constructor() {
super();
this.context = '';
this._i18n = createInstance();
this.lang = this._i18n.language;
this.nextcloudAuthUrl = '';
this.nextcloudName = 'Nextcloud';
this.nextcloudWebDavUrl = '';
this.nextcloudPath = '';
this.nextcloudFileURL = '';
this.nextcloudStoreSession = false;
this.dropArea = null;
this.allowedMimeTypes = '';
this.enabledTargets = 'local';
this.buttonLabel = '';
this.disabled = false;
this.decompressZip = false;
this._queueKey = 0;
this.activeTarget = 'local';
this.isDialogOpen = false;
this.firstOpen = true;
this.nextcloudAuthInfo = '';
this.maxFileSize = '';
this.multipleFiles = Number.MAX_VALUE;
this.initialFileHandlingState = {target: '', path: ''};
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-mini-spinner': MiniSpinner,
'dbp-nextcloud-file-picker': NextcloudFilePicker,
'dbp-clipboard': Clipboard,
};
}
/**
* See: https://lit-element.polymer-project.org/guide/properties#initialize
*/
static get properties() {
return {
...super.properties,
context: {type: String, attribute: 'context'},
lang: {type: String},
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'},
nextcloudAuthInfo: {type: String, attribute: 'nextcloud-auth-info'},
nextcloudStoreSession: {type: Boolean, attribute: 'nextcloud-store-session'},
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'},
maxFileSize: {type: Number, attribute: 'max-file-size'},
multipleFiles: {type: Number, attribute: 'number-of-files'},
initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
};
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'lang':
this._i18n.changeLanguage(this.lang);
break;
case 'enabledTargets':
if (!this.hasEnabledSource(this.activeTarget)) {
this.activeTarget = this.enabledTargets.split(',')[0];
}
break;
case 'isDialogOpen':
if (this.isDialogOpen) {
// this.setAttribute("dialog-open", "");
this.openDialog();
} else {
this.removeAttribute('dialog-open');
// this.closeDialog();
}
break;
case 'initialFileHandlingState':
//check if default destination is set
if (this.firstOpen) {
this.nextcloudPath = this.initialFileHandlingState.path;
}
break;
case 'activeTarget':
if (this.activeTarget === 'nextcloud') {
this.loadWebdavDirectory();
}
break;
}
});
super.update(changedProperties);
}
connectedCallback() {
super.connectedCallback();
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, this._('#fileElem'))
);
this._('nav.modal-nav').addEventListener('scroll', this.handleScroll.bind(this));
this._('.right-paddle').addEventListener(
'click',
this.handleScrollRight.bind(this, this._('nav.modal-nav'))
);
this._('.left-paddle').addEventListener(
'click',
this.handleScrollLeft.bind(this, this._('nav.modal-nav'))
);
});
}
disconnectedCallback() {
super.disconnectedCallback();
}
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
highlight(e) {
this.dropArea.classList.add('highlight');
}
unhighlight(e) {
this.dropArea.classList.remove('highlight');
}
handleDrop(e) {
if (this.disabled) {
return;
}
let dt = e.dataTransfer;
// console.dir(dt);
let files = dt.files;
this.handleFiles(files);
}
async handleChange(element) {
let fileElem = element;
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>}
*/
async handleFiles(files) {
// console.log('handleFiles: files.length = ' + files.length);
// this.dispatchEvent(new CustomEvent("dbp-file-source-selection-start",
// { "detail": {}, bubbles: true, composed: true }));
let fileCount = files.length;
await commonUtils.asyncArrayForEach(files, async (file, index) => {
if (file.size === 0) {
console.log("file '" + file.name + "' has size=0 and is denied!");
return;
}
if (!this.checkSize(file)) {
return;
}
// 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, index, array) => {
fileCount = index === array.length - 1 ? fileCount : fileCount + 1;
this.sendFileEvent(file, fileCount);
}
);
return;
} else if (this.allowedMimeTypes && !this.checkFileType(file)) {
return;
}
await this.sendFileEvent(file, fileCount);
});
// this.dispatchEvent(new CustomEvent("dbp-file-source-selection-finished",
// { "detail": {}, bubbles: true, composed: true }));
const event = new CustomEvent('dbp-file-source-file-upload-finished', {
detail: {count: fileCount},
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
this.closeDialog();
}
/**
* @param file
* @param maxUpload
*/
sendFileEvent(file, maxUpload) {
this.sendSource();
MicroModal.close(this._('#modal-picker'));
const data = {file: file, maxUpload: maxUpload};
const event = new CustomEvent('dbp-file-source-file-selected', {
detail: data,
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
sendSource() {
let data = {};
if (this.activeTarget === 'nextcloud') {
data = {
target: this.activeTarget,
path: this._('#nextcloud-file-picker').directoryPath,
};
} else {
data = {target: this.activeTarget};
}
this.sendSetPropertyEvent('initial-file-handling-state', data);
}
checkFileType(file) {
const i18n = this._i18n;
// 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}`
);
send({
summary: i18n.t('file-source.mime-type-title'),
body: i18n.t('file-source.mime-type-body'),
type: 'danger',
timeout: 5,
});
return false;
}
return true;
}
checkSize(file) {
const i18n = this._i18n;
if (this.maxFileSize !== '' && this.maxFileSize * 1000 <= file.size) {
send({
summary: i18n.t('file-source.too-big-file-title'),
body: i18n.t('file-source.too-big-file-body', {
size: humanFileSize(this.maxFileSize * 1000, true),
}),
type: 'danger',
timeout: 5,
});
return false;
}
return true;
}
hasEnabledSource(source) {
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').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
console.error('Loading of ' + file.name + ' failed: ' + e.message);
}
);
// no suitable files found
if (filesToHandle.length === 0) {
const i18n = this._i18n;
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);
send({
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,
});
this.dispatchEvent(event);
}
loadWebdavDirectory() {
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.checkLocalStorage().then((contents) => {
if (filePicker.webDavClient !== null) {
filePicker.loadDirectory(filePicker.directoryPath);
}
});
}
}
openDialog() {
if (this.enabledTargets.includes('nextcloud')) {
this.loadWebdavDirectory();
}
if (this.enabledTargets.includes('clipboard') && this._('#clipboard-file-picker')) {
this._('#clipboard-file-picker').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;
const filePicker = this._('#nextcloud-file-picker');
if (filePicker) {
filePicker.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;
const filePicker = this._('#nextcloud-file-picker');
if (filePicker && filePicker.webDavClient !== null) {
filePicker.loadDirectory(this.initialFileHandlingState.path);
//console.log("load default nextcloud source", this.initialFileHandlingState.target);
}
this.firstOpen = false;
}
}
closeDialog() {
this.sendSource();
if (this.enabledTargets.includes('nextcloud')) {
const filePicker = this._('#nextcloud-file-picker');
if (filePicker && filePicker.tabulatorTable) {
filePicker.tabulatorTable.deselectRow();
if (filePicker._('#select_all')) {
filePicker._('#select_all').checked = false;
}
}
}
if (this.enabledTargets.includes('clipboard')) {
const filePicker = this._('#clipboard-file-picker');
if (filePicker && filePicker.tabulatorTable) {
filePicker.numberOfSelectedFiles = 0;
filePicker.tabulatorTable.deselectRow();
if (filePicker._('#select_all')) {
filePicker._('#select_all').checked = false;
}
}
}
MicroModal.close(this._('#modal-picker'));
}
getClipboardHtml() {
if (this.enabledTargets.includes('clipboard')) {
return html`
<dbp-clipboard
id="clipboard-file-picker"
mode="file-source"
subscribe="clipboard-files:clipboard-files"
lang="${this.lang}"
auth-url="${this.nextcloudAuthUrl}"
enabled-targets="${this.enabledTargets}"
nextcloud-auth-url="${this.nextcloudAuthUrl}"
nextcloud-web-dav-url="${this.nextcloudWebDavUrl}"
nextcloud-name="${this.nextcloudName}"
nextcloud-file-url="${this.nextcloudFileURL}"
allowed-mime-types="${this.allowedMimeTypes}"
@dbp-clipboard-file-picker-file-downloaded="${(event) => {
this.sendFileEvent(event.detail.file);
}}"></dbp-clipboard>
`;
}
return html``;
}
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}"
subscribe="html-overrides,auth"
auth-url="${this.nextcloudAuthUrl}"
web-dav-url="${this.nextcloudWebDavUrl}"
nextcloud-name="${this.nextcloudName}"
nextcloud-file-url="${this.nextcloudFileURL}"
?store-nextcloud-session="${this.nextcloudStoreSession}"
auth-info="${this.nextcloudAuthInfo}"
allowed-mime-types="${this.allowedMimeTypes}"
max-selected-items="${this.multipleFiles}"
@dbp-nextcloud-file-picker-file-downloaded="${(event) => {
this.sendFileEvent(event.detail.file, event.detail.maxUpload);
}}"></dbp-nextcloud-file-picker>
`;
}
return html``;
}
static get styles() {
// language=css
return css`
${commonStyles.getThemeCSS()}
${commonStyles.getGeneralCSS()}
${commonStyles.getButtonCSS()}
${commonStyles.getModalDialogCSS()}
${fileHandlingStyles.getFileHandlingCss()}
p {
margin-top: 0;
}
.block {
margin-bottom: 10px;
}
#dropArea {
border: var(
--dbp-border,
var(--FUBorderWidth, 2px) var(--FUBorderStyle, dashed)
var(--FUBBorderColor, black)
);
border-style: var(--FUBorderStyle, dashed);
border-radius: var(--FUBorderRadius, var(--dbp-border-radius, 0));
border-width: var(--FUBorderWidth, 2px);
width: auto;
margin: var(--FUMargin, 0px);
padding: var(--FUPadding, 20px);
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
#dropArea.highlight {
border-color: var(--FUBorderColorHighlight, purple);
}
#clipboard-file-picker {
width: 100%;
height: 100%;
}
.paddle {
position: absolute;
top: 0px;
padding: 0px 5px;
box-sizing: content-box;
height: 100%;
}
.paddle::before {
background-color: var(--dbp-background);
opacity: 0.8;
content: '';
width: 100%;
height: 100%;
position: absolute;
left: 0;
}
.right-paddle {
right: 0px;
}
.left-paddle {
left: 0px;
}
.nav-wrapper {
position: relative;
display: block;
overflow-x: auto;
border: none;
}
.paddles {
display: none;
}
.modal-nav {
height: 100%;
}
@media only screen and (orientation: portrait) and (max-width: 768px) {
#dropArea {
height: 100%;
}
}
@media only screen and (orientation: portrait) and (max-width: 340px) {
.paddles {
display: inherit;
}
}
`;
}
render() {
const i18n = this._i18n;
let allowedMimeTypes = this.allowedMimeTypes;
if (this.decompressZip && this.allowedMimeTypes !== '') {
allowedMimeTypes += ',application/zip,application/x-zip-compressed';
}
let inputFile = html``;
if (this.multipleFiles > 1 || this.multipleFiles === true) {
inputFile = html`
<input
?disabled="${this.disabled}"
type="file"
id="fileElem"
multiple
accept="${mimeTypesToAccept(allowedMimeTypes)}"
name="file" />
`;
} else {
inputFile = html`
<input
?disabled="${this.disabled}"
type="file"
id="fileElem"
accept="${mimeTypesToAccept(allowedMimeTypes)}"
name="file" />
`;
}
return html`
<!--
<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">
<div class="modal-overlay" tabindex="-1" data-micromodal-close>
<div
class="modal-container"
role="dialog"
aria-modal="true"
aria-labelledby="modal-picker-title">
<div class="nav-wrapper modal-nav">
<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';
}}"
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>
</div>
<div
title="${i18n.t('file-source.clipboard')}"
@click="${() => {
this.activeTarget = 'clipboard';
}}"
class="${classMap({
active: this.activeTarget === 'clipboard',
hidden: !this.hasEnabledSource('clipboard'),
})}">
<dbp-icon class="nav-icon" name="clipboard"></dbp-icon>
<p>${i18n.t('file-source.clipboard')}</p>
</div>
</nav>
<div class="paddles">
<dbp-icon
class="left-paddle paddle hidden"
name="chevron-left"
class="close-icon"></dbp-icon>
<dbp-icon
class="right-paddle paddle"
name="chevron-right"
class="close-icon"></dbp-icon>
</div>
</div>
<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>
</button>
<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>${i18n.t('intro')}</p>
</div>
${inputFile}
<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 === '',
})}">
${this.getNextcloudHtml()}
</div>
<div
class="source-main ${classMap({
hidden: this.activeTarget !== 'clipboard',
})}">
${this.getClipboardHtml()}
</div>
</main>
</div>
</div>
</div>
`;
}
}