Skip to content
Snippets Groups Projects
Select Git revision
  • 03623666df69328b1e15152c0f3c842e340a192d
  • main default protected
  • renovate/lock-file-maintenance
  • demo protected
  • person-select-custom
  • dbp-translation-component
  • icon-set-mapping
  • port-i18next-parser
  • remove-sentry
  • favorites-and-recent-files
  • revert-6c632dc6
  • lit2
  • advertisement
  • wc-part
  • automagic
  • publish
  • wip-cleanup
  • demo-file-handling
18 results

file-source.js

Blame
  • file-source.js 28.08 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-dark, 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-base-light);
                    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>
              `;
        }
    }