@digital-blueprint/esign-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/esign-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/esign-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/esign-app/) | [Esign Bundle](https://gitlab.tugraz.
1,342 lines (1,194 loc) • 65.4 kB
JavaScript
import {createInstance} from './i18n.js';
import {css, unsafeCSS} from 'lit';
import * as utils from './utils';
import * as commonUtils from '@dbp-toolkit/common/utils';
import {BaseLitElement} from './base-element.js';
import {SignatureEntry} from './signature-entry.js';
import {getPDFSignatureCount} from './utils';
import { send } from '@dbp-toolkit/common/notification';
import { humanFileSize } from '@dbp-toolkit/common/i18next';
export default class DBPSignatureLitElement extends BaseLitElement {
constructor() {
super();
this._i18n = createInstance();
this.queuedFiles = [];
this.queuedFilesCount = 0;
this.uploadInProgress = false;
this.queueBlockEnabled = false;
this._queueKey = 0;
this.queuedFilesAnnotationModes = [];
this.signingProcessEnabled = false;
this.queuedFilesAnnotationSaved = [];
this.queuedFilesEnabledAnnotations = [];
this.queuedFilesNeedsPlacement = new Map();
this.queuedFilesSignaturePlacements = [];
this.queuedFilesPlacementModes = [];
this.externalAuthInProgress = false;
this.tableQueuedFilesTable = null;
this.tableSignedFilesTable = null;
this.tableFailedFilesTable = null;
this.queuedFilesOptions = {};
this.signedFilesOptions = {};
this.failedFilesOptions = {};
this.queuedFilesTableExpanded = false;
this.queuedFilesTableAllSelected = false;
this.queuedFilesTableCollapsible = false;
this.signedFilesTableExpanded = false;
this.signedFilesTableCollapsible = false;
this.failedFilesTableExpanded = false;
this.failedFilesTableCollapsible = false;
this.currentFile = {};
this.currentFileName = '';
this.currentFilePlacementMode = '';
this.currentFileSignaturePlacement = {};
this.currentKey = '';
this.queuedFilesAnnotations = [];
this.queuedFilesAnnotationsCount = 0;
this.uploadStatusFileName = '';
this.uploadStatusText = '';
this.signingProcessActive = false;
this.signaturePlacementInProgress = false;
this.signedFilesToDownload = 0;
this.withSigBlock = false;
this.currentPreviewQueueKey = '';
this.allowAnnotating = false;
this.isAnnotationViewVisible = false;
this.addAnnotationInProgress = false;
// will be set in function update
this.fileSourceUrl = '';
this.fileSource = '';
this.nextcloudDefaultDir = '';
this.signedFiles = [];
this.signedFilesCount = 0;
this.signedFilesCountToReport = 0;
this.errorFiles = [];
this.errorFilesCount = 0;
this.errorFilesCountToReport = 0;
this.selectedFiles = [];
this.selectedFilesProcessing = false;
this.initialQueuedFilesCount = 0;
this.positionButtonObserverAdded = false;
this.positionButtonObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
const collapsedSections = this.tableQueuedFilesTable.shadowRoot.querySelectorAll('.tabulator-responsive-collapse');
// Add event listener to collapsed cells buttons.
// cellClick() is not run on collapsed fields.
collapsedSections.forEach((section) => {
const positionToggler = section.querySelector('.toggle-item');
if (positionToggler) {
if (positionToggler.classList.contains('event-listener-added')) {
return;
} else {
positionToggler.classList.add('event-listener-added');
const rows = this.tableQueuedFilesTable.getRows();
const rowId = positionToggler.getAttribute('data-row-id');
const row = rows[rowId - 1];
if (row) {
const cell = row.getCell('positioning');
positionToggler.addEventListener('click', (event) => {
this.handlePositionButtonClickEvent(event, cell);
});
}
}
}
});
}
});
});
}
static get properties() {
return {
...super.properties,
lang: {type: String},
entryPointUrl: {type: String, attribute: 'entry-point-url'},
nextcloudWebAppPasswordURL: {type: String, attribute: 'nextcloud-web-app-password-url'},
nextcloudWebDavURL: {type: String, attribute: 'nextcloud-webdav-url'},
nextcloudName: {type: String, attribute: 'nextcloud-name'},
nextcloudFileURL: {type: String, attribute: 'nextcloud-file-url'},
nextcloudAuthInfo: {type: String, attribute: 'nextcloud-auth-info'},
signedFiles: {type: Array, attribute: false},
signedFilesCount: {type: Number, attribute: false},
signedFilesCountToReport: {type: Number, attribute: false},
signedFilesToDownload: {type: Number, attribute: false},
queuedFilesCount: {type: Number, attribute: false},
errorFiles: {type: Array, attribute: false},
errorFilesCount: {type: Number, attribute: false},
errorFilesCountToReport: {type: Number, attribute: false},
uploadInProgress: {type: Boolean, attribute: false},
uploadStatusFileName: {type: String, attribute: false},
uploadStatusText: {type: String, attribute: false},
signingProcessEnabled: {type: Boolean, attribute: false},
signingProcessActive: {type: Boolean, attribute: false},
queueBlockEnabled: {type: Boolean, attribute: false},
currentFile: {type: Object, attribute: false},
currentFileName: {type: String, attribute: false},
currentKey: {type: String, attribute: false},
signaturePlacementInProgress: {type: Boolean, attribute: false},
withSigBlock: {type: Boolean, attribute: false},
isSignaturePlacement: {type: Boolean, attribute: false},
allowAnnotating: {type: Boolean, attribute: 'allow-annotating'},
isAnnotationViewVisible: {type: Boolean, attribute: false},
queuedFilesAnnotations: {type: Array, attribute: false},
queuedFilesAnnotationsCount: {type: Number, attribute: false},
addAnnotationInProgress: {type: Boolean, attribute: false},
queuedFilesAnnotationModes: {type: Array, attribute: false},
queuedFilesAnnotationSaved: {type: Array, attribute: false},
fileHandlingEnabledTargets: {type: String, attribute: 'file-handling-enabled-targets'},
queuedFilesTableExpanded: {type: Boolean, attribute: false},
queuedFilesTableAllSelected: {type: Boolean, attribute: false},
queuedFilesTableCollapsible: {type: Boolean, attribute: false},
signedFilesTableExpanded: {type: Boolean, attribute: false},
signedFilesTableCollapsible: {type: Boolean, attribute: false},
failedFilesTableExpanded: {type: Boolean, attribute: false},
failedFilesTableCollapsible: {type: Boolean, attribute: false},
selectedFiles: {type: Array, attribute: false},
};
}
/**
* @param file
* @returns {Promise<string>} key of the queued item
*/
async queueFile(file) {
this._queueKey++;
const key = String(this._queueKey);
this.queuedFiles[key] = new SignatureEntry(key, file);
this.updateQueuedFilesCount();
return key;
}
/**
* @param file
* @returns {Promise<string>} key of the re-queued item
*/
async reQueueFile(file) {
const key = this.currentKey;
this.queuedFiles[key] = new SignatureEntry(key, file);
this.updateQueuedFilesCount();
return key;
}
/**
* Takes a file off of the queue
*
* @param key
* @returns {SignatureEntry} entry
*/
takeFileFromQueue(key) {
const entry = this.queuedFiles[key];
delete this.queuedFiles[key];
this.updateQueuedFilesCount();
return entry;
}
/**
* @param {*} key
* @param {*} name
*/
async showAnnotationView(key, name) {
this.queuedFilesAnnotationModes[key] = name;
if (this.signingProcessEnabled) {
return;
}
if (name === 'text-selected') {
const file = this.getQueuedFile(key);
this.currentFile = file;
this.currentPreviewQueueKey = key;
this.addAnnotationInProgress = true;
const viewTag = 'dbp-pdf-annotation-view';
this._(viewTag).setAttribute('key', key);
this._(viewTag).setAnnotationRows(this.queuedFilesAnnotations[key]);
this.isAnnotationViewVisible = true;
this.enableAnnotationsForKey(key);
} else {
this.disableAnnotationsForKey(key);
this.queuedFilesAnnotationSaved[key] = false;
if (this.currentPreviewQueueKey === key) {
this.isAnnotationViewVisible = false;
}
}
}
/**
*
* @param {*} event
*/
processAnnotationEvent(event) {
let annotationDetails = event.detail;
let key = this.currentPreviewQueueKey;
this.queuedFilesAnnotations[key] = annotationDetails.annotationRows;
this.isAnnotationViewVisible = false;
this.addAnnotationInProgress = false;
this.queuedFilesAnnotationModes[this.currentPreviewQueueKey] = 'text-selected';
this.queuedFilesAnnotationSaved[this.currentPreviewQueueKey] = true;
}
/**
*
* @param {*} event
*/
processAnnotationCancelEvent(event) {
let key = this.currentPreviewQueueKey;
this.queuedFilesAnnotations[key] = [];
this.queuedFilesAnnotations[key] = undefined;
this.disableAnnotationsForKey(key);
this.queuedFilesAnnotationModes[this.currentPreviewQueueKey] = 'no-text';
this.queuedFilesAnnotationSaved[this.currentPreviewQueueKey] = false;
}
/**
* Hides the PdfAnnotationView
*/
hideAnnotationView() {
if (
this.queuedFilesAnnotationSaved[this.currentPreviewQueueKey] !== undefined &&
this.queuedFilesAnnotationSaved[this.currentPreviewQueueKey]
) {
this.queuedFilesAnnotationModes[this.currentPreviewQueueKey] = 'text-selected';
} else {
this.queuedFilesAnnotationModes[this.currentPreviewQueueKey] = 'no-text';
}
this.isAnnotationViewVisible = false;
this.addAnnotationInProgress = false;
}
/**
* Add multiple annotations to a PDF file
*
* @param file
* @param annotations
* @returns {Promise<File>} file given as parameter, but with annotations
*/
async addAnnotationsToFile(file, annotations) {
// We need to work with the AnnotationFactory because the pdf file is broken if
// we add the multiple annotations to the file itself
let pdfFactory = await utils.getAnnotationFactoryFromFile(file);
const activityNameDE = this.activity.getName('de');
const activityNameEN = this.activity.getName('en');
await commonUtils.asyncObjectForEach(annotations, async (annotation) => {
console.log('annotation', annotation);
const annotationType = (annotation.annotationType || '').trim();
const value = (annotation.value || '').trim();
if (annotationType === '' || value === '') {
return;
}
const annotationTypeData = utils.getAnnotationTypes(annotationType);
pdfFactory = await utils.addKeyValuePdfAnnotationsToAnnotationFactory(
pdfFactory,
activityNameDE,
activityNameEN,
this.auth['user-full-name'],
annotationType,
annotationTypeData.name.de,
annotationTypeData.name.en,
value
);
});
// output the AnnotationFactory as File again
return utils.writeAnnotationFactoryToFile(pdfFactory, file);
}
/**
* Remove an annotation of a file on the queue
*
* @param key
* @param id
*/
removeAnnotation(key, id) {
if (this.queuedFilesAnnotations[key] && this.queuedFilesAnnotations[key][id]) {
delete this.queuedFilesAnnotations[key][id];
// we just need this so the UI will update
this.queuedFilesAnnotationsCount--;
}
}
/**
* Takes the annotations of a file off of the queue
*
* @param key
*/
takeAnnotationsFromQueue(key) {
const annotations = this.queuedFilesAnnotations[key];
delete this.queuedFilesAnnotations[key];
this.disableAnnotationsForKey(key);
return annotations;
}
/**
* Checks if annotations are enabled for an annotation key
*
* @param key
* @returns {boolean} true if annotations are enabled for annotation key
*/
isAnnotationsEnabledForKey(key) {
return this.queuedFilesEnabledAnnotations.includes(key);
}
/**
* Enables annotations for an annotation key
*
* @param key
*/
enableAnnotationsForKey(key) {
if (!this.isAnnotationsEnabledForKey(key)) {
this.queuedFilesEnabledAnnotations.push(key);
}
}
/**
* Disables annotations for an annotation key
*
* @param key
*/
disableAnnotationsForKey(key) {
let i = 0;
// remove all occurrences of the value "key" in array this.queuedFilesEnabledAnnotations
while (i < this.queuedFilesEnabledAnnotations.length) {
if (this.queuedFilesEnabledAnnotations[i] === key) {
this.queuedFilesEnabledAnnotations.splice(i, 1);
} else {
++i;
}
}
}
/**
* Update an annotation of a file on the queue
*
* @param key
* @param id
* @param annotationKey
* @param value
*/
updateAnnotation(key, id, annotationKey, value) {
if (this.queuedFilesAnnotations[key] && this.queuedFilesAnnotations[key][id]) {
this.queuedFilesAnnotations[key][id][annotationKey] = value;
}
}
getQueuedFile(key) {
return this.queuedFiles[key];
}
/**
* Remove selected files from the file queue
* @param {Array} filesToRemove - array of index to remove
*/
clearQueuedFiles(filesToRemove) {
if (!Array.isArray(filesToRemove) || filesToRemove.length < 1) return;
for (const fileKey of filesToRemove) {
// Remove annotation of selected rows form queuedFilesAnnotations
this.queuedFilesAnnotations.forEach((annotation, index) => {
if (index == fileKey) {
delete this.queuedFilesAnnotations[index];
}
});
// Remove files of selected rows from queueFiles
this.queuedFiles.forEach((file, index) => {
if (index == fileKey) {
delete this.queuedFiles[index];
}
});
}
this.selectedFiles = [];
this.queuedFilesAnnotationsCount = this.getRealLength(this.queuedFilesAnnotations);
this.updateQueuedFilesCount();
this.tableQueuedFilesTable.tabulatorTable.redraw(true);
}
/**
* Get the real length of an array containing holes (sparse array)
* @param {Array} array
* @returns {number}
*/
getRealLength(array) {
return Object.keys(array).filter(key => !isNaN(key)).length;
}
updateQueuedFilesCount() {
this.queuedFilesCount = Object.keys(this.queuedFiles).length;
if (!this.queueBlockEnabled && this.queuedFilesCount > 0) {
this.queueBlockEnabled = true;
}
return this.queuedFilesCount;
}
/**
* @param file
* @param params
* @param annotations
* @returns {Promise<void>}
*/
async uploadFile(file, params = {}, annotations = []) {
this.uploadInProgress = true;
this.uploadStatusFileName = file.name;
let formData = new FormData();
// add annotations
if (annotations.length > 0) {
file = await this.addAnnotationsToFile(file, annotations);
// Also send annotations to the server so they get included in the signature block
let userText = [];
for (let annotation of annotations) {
const annotationTypeData = utils.getAnnotationTypes(annotation['annotationType']);
userText.push({
description: `${annotationTypeData.name.de || ''} / ${
annotationTypeData.name.en || ''
}`,
value: annotation['value'],
});
}
formData.append('user_text', JSON.stringify(userText));
}
let url = new URL(this.fileSourceUrl);
formData.append('file', file);
for (let key in params) {
formData.append(key, params[key]);
}
// I got a 60s timeout in Google Chrome and found no way to increase that
await fetch(url, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.auth.token,
},
body: formData,
})
.then(response => {
/* Done. Inform the user */
console.log(`Status: ${response.status} for file ${file.name}`);
this.sendFinishedEvent(response, file);
})
.catch(response => {
/* Error. Inform the user */
if (response.message) {
send({
summary: 'Error!',
body: response.message,
type: 'danger',
timeout: 15,
});
console.log(`Error message: ${response.message}`);
}
this.sendFinishedEvent(response, file);
});
this.uploadInProgress = false;
}
async sendFinishedEvent(response, file) {
if (response === undefined) {
return;
}
let data = {
fileName: file.name,
status: response.status,
json: {'hydra:description': ''},
};
if (response.status !== 201 && response.message) {
data.json = { 'hydra:description': response.message};
}
try {
await response.json().then((json) => {
data.json = json;
});
} catch (e) {
console.error(e);
}
data.file = file;
this.onFileUploadFinished(data);
}
onFileSourceSwitch(event) {
if (event.detail.source) {
this.fileSource = event.detail.source;
}
if (event.detail.nextcloud) {
this.nextcloudDefaultDir = event.detail.nextcloud;
}
event.preventDefault();
}
/**
* Convert files to binary async
*/
async convertFiles() {
let files = [];
for (const file of this.signedFiles) {
const arr = utils.convertDataURIToBinary(file.contentUrl);
const binaryFile = new File([arr], file.name, {
type: utils.getDataURIContentType(file.contentUrl),
});
files.push(binaryFile);
}
return files;
}
/**
* Open Filesink for multiple files
*/
async zipDownloadClickHandler() {
// add all signed pdf-files
const files = await this.convertFiles();
this._('#file-sink').files = [...files];
this.signedFilesToDownload = files.length;
this._('#zip-download-button').stop();
// mark downloaded files buttons
const spans = this.shadowRoot.querySelectorAll(
'.file-block > div.header > span.filename > span.bold-filename'
);
spans.forEach((span) => {
span.classList.remove('bold-filename');
});
}
/**
* @param data
*/
onFileUploadFinished(data) {
console.log('Override me');
}
/**
* Open Filesink for a single File
*
* @param file
* @param id of element to mark
*/
async downloadFileClickHandler(file, id) {
let files = [];
const arr = utils.convertDataURIToBinary(file.contentUrl);
const binaryFile = new File([arr], file.name, {
type: utils.getDataURIContentType(file.contentUrl),
});
files.push(binaryFile);
this.signedFilesToDownload = files.length;
this._('#file-sink').files = [...files];
// mark downloaded files button
const span = this.shadowRoot.querySelector(
'#' + id + ' > div.header > span.filename > span.bold-filename'
);
if (span) {
span.classList.remove('bold-filename');
}
}
async _updateNeedsPlacementStatus(id) {
let entry = this.queuedFiles[id];
let sigCount = await getPDFSignatureCount(entry.file);
this.queuedFilesNeedsPlacement.delete(id);
if (sigCount > 0) this.queuedFilesNeedsPlacement.set(id, true);
}
storePDFData(event) {
let placement = event.detail;
let placementMode = placement.signaturePlacementMode;
let key = this.currentPreviewQueueKey;
this.queuedFilesSignaturePlacements[key] = placement;
this.queuedFilesPlacementModes[key] = placementMode;
this.signaturePlacementInProgress = false;
}
/**
* Called when preview is "canceled"
*
* @param event
*/
hidePDF(event) {
if (this.queuedFilesSignaturePlacements[this.currentPreviewQueueKey] !== undefined) {
// If canceled when try to set to manual mode remove placement settings (auto is the default)
if (this.queuedFilesSignaturePlacements[this.currentPreviewQueueKey].signaturePlacementMode === 'manual') {
this.queuedFilesSignaturePlacements.splice(parseInt(this.currentPreviewQueueKey), 1);
this.queuedFilesPlacementModes[this.currentPreviewQueueKey] = 'auto';
} else {
// If canceled when try to set automatic set back to manual
this.queuedFilesSignaturePlacements[this.currentPreviewQueueKey].signaturePlacementMode === 'manual';
this.queuedFilesPlacementModes[this.currentPreviewQueueKey] = 'manual';
}
// Re render text switch
this.setQueuedFilesTabulatorTable();
}
this.signaturePlacementInProgress = false;
}
queuePlacementSwitch(key, name) {
this.queuedFilesPlacementModes[key] = name;
this.showPreview(key, true);
this.requestUpdate();
}
queuePlacement(key, name, showSignature = true) {
this.queuedFilesPlacementModes[key] = name;
this.showPreview(key, showSignature);
this.requestUpdate();
}
endSigningProcessIfQueueEmpty() {
if (this.queuedFilesCount === 0 && this.signingProcessActive) {
this.signingProcessActive = false;
}
}
/**
* @param ev
*/
onFileSelected(ev) {
this.queueFile(ev.detail.file);
}
/**
* Re-Upload all failed files
*/
reUploadAllClickHandler() {
const that = this;
// we need to make a copy and reset the queue or else our queue will run crazy
const errorFilesCopy = {...this.errorFiles};
this.errorFiles = [];
this.errorFilesCount = 0;
commonUtils.asyncObjectForEach(errorFilesCopy, async (file, id) => {
await this.fileQueueingClickHandler(file.file, id);
});
that._('#re-upload-all-button').stop();
}
/**
* Queues a failed pdf-file again
*
* @param file
* @param id
*/
async fileQueueingClickHandler(file, id) {
this.takeFailedFileFromQueue(id);
return this.queueFile(file);
}
/**
* Shows the preview
*
* @param key
* @param withSigBlock
* @param viewOnly
*/
async showPreview(key, withSigBlock = false, viewOnly = false) {
if (this.signingProcessEnabled) {
return;
}
const entry = this.getQueuedFile(key);
this.currentFile = entry.file;
this.currentPreviewQueueKey = key;
this.withSigBlock = withSigBlock;
let placementData = this.queuedFilesSignaturePlacements[key];
if (viewOnly) {
placementData = {};
}
await this._('dbp-pdf-preview').showPDF(
entry.file,
withSigBlock, //this.queuedFilesPlacementModes[key] === "manual",
placementData
);
}
onLanguageChanged(e) {
this.lang = e.detail.lang;
}
/**
* Takes a failed file off of the queue
*
* @param key
*/
takeFailedFileFromQueue(key) {
const file = this.errorFiles.splice(key, 1);
this.errorFilesCount = Object.keys(this.errorFiles).length;
return file;
}
clearSignedFiles() {
this.signedFiles = [];
this.signedFilesCount = 0;
}
clearErrorFiles() {
this.errorFiles = [];
this.errorFilesCount = 0;
}
/**
* Return true if the key of the file is in the selectedFiles
* @param {string} key
* @returns {boolean}
*/
fileIsSelectedFile(key){
return this.selectedFiles.some((file) => file.key === key);
}
tabulatorTableHandleCollapse(event) {
if (event.detail.tableId === 'table-queued-files') {
this.queuedFilesTableCollapsible = event.detail.isCollapsible;
// If the table is not collapsible display the 'Expand all' button
if (!this.queuedFilesTableCollapsible) {
this.queuedFilesTableExpanded = false;
}
// Update table data when collapsing or expanding rows otherwise action buttons will not be shown
this.setQueuedFilesTabulatorTable();
}
if (event.detail.tableId === 'table-signed-files') {
this.signedFilesTableCollapsible = event.detail.isCollapsible;
// Update table data when collapsing or expanding rows
this.setSignedFilesTabulatorTable();
}
if (event.detail.tableId === 'table-failed-files') {
this.failedFilesTableCollapsible = event.detail.isCollapsible;
// Update table data when collapsing or expanding rows
this.setFailedFilesTabulatorTable();
}
}
/**
* Handle Expand/Collapse-all button state
* @param event - dbp-tabulator-table-render-complete-event
*/
tabulatorTableHandleRenderCompleted(event) {
const tableId = event.detail.tableId;
const table = this._(`#${tableId}`);
const collapsableRows = table.shadowRoot.querySelectorAll('.tabulator-responsive-collapse');
const collapsableRowsCount = collapsableRows.length;
let expandedColumns = 0;
collapsableRows.forEach((row) => {
if (window.getComputedStyle(row).display === 'block') {
expandedColumns++;
}
});
if (event.detail.tableId === 'table-queued-files') {
expandedColumns == collapsableRowsCount ? this.queuedFilesTableExpanded = true : this.queuedFilesTableExpanded = false;
}
if (event.detail.tableId === 'table-signed-files') {
expandedColumns == collapsableRowsCount ? this.signedFilesTableExpanded = true : this.signedFilesTableExpanded = false;
}
if (event.detail.tableId === 'table-failed-files') {
expandedColumns == collapsableRowsCount ? this.failedFilesTableExpanded = true : this.failedFilesTableExpanded = false;
}
}
handlePdfModalClosing() {
this._('#pdf-preview').close();
}
handleAnnotationModalClosing() {
this._('#annotation-view').close();
}
handleModalClosed(event) {
if (event.detail.id === 'pdf-preview-modal') {
if (this.signaturePlacementInProgress) {
this.hidePDF();
}
}
if (event.detail.id === 'external-auth-modal') {
this.stopSigningProcess();
}
if (event.detail.id === 'annotation-view-modal') {
this.setQueuedFilesTabulatorTable();
const annotationModal = this._('#annotation-view');
const pdfAnnotationView = this._('dbp-pdf-annotation-view');
// Don't allow closing the modal if the annotation is not valid
if (!pdfAnnotationView.validateValues(true)) {
annotationModal.open();
pdfAnnotationView.validateValues();
}
}
}
/**
* Update selectedRows on selection changes
* @param {object} tableEvent
*/
handleTableSelection(tableEvent) {
const allSelectedRows = tableEvent.detail.allselected;
const selectedRows = tableEvent.detail.selected;
const deSelectedRows = tableEvent.detail.deselected;
// Add selected files
if (Array.isArray(selectedRows) && selectedRows.length > 0) {
selectedRows.forEach(selectedRow => {
const rowIndex = String(selectedRow.getIndex());
const rowData = selectedRow.getData();
const fileNameCell = rowData.fileName;
const fileKey = rowData.index;
// Remove html tags from filename (the warning tooltip)
const fileName = fileNameCell.replace(/<[^>]*>/g, '').trim();
const existingIndex = this.selectedFiles.findIndex(row => row.key === rowIndex);
if (existingIndex === -1) {
this.selectedFiles = [
...this.selectedFiles,
{
key: fileKey,
filename: fileName
}
];
}
});
}
// Remove selected files
if (Array.isArray(deSelectedRows) && deSelectedRows.length > 0) {
deSelectedRows.forEach(deSelectedRow => {
const rowIndex = String(deSelectedRow.getIndex());
const deselectedIndex = this.selectedFiles.findIndex(row => row.key === rowIndex);
this.selectedFiles = [
...this.selectedFiles.slice(0, deselectedIndex),
...this.selectedFiles.slice(deselectedIndex + 1)
];
});
}
// If all rows are selected toggle select-all button to deselect-all
if (allSelectedRows.length === this.queuedFilesCount) {
this.queuedFilesTableAllSelected = true;
}
if (selectedRows.length === 0) {
this.queuedFilesTableAllSelected = false;
}
}
startPositionButtonObserver() {
if (this.positionButtonObserverAdded) {
return;
}
if (this.tableQueuedFilesTable) {
const table = this._('#table-queued-files');
if (table && table.shadowRoot) {
this.positionButtonObserver.observe(table.shadowRoot, {
childList: true,
subtree: true
});
this.positionButtonObserverAdded = true;
}
}
}
stopPositionButtonObserver() {
this.positionButtonObserver.disconnect();
}
getActionButtonsHtml(id, annotations=true) {
const i18n = this._i18n;
const fileName = this.queuedFiles[id].file.name;
const annotationCount = Array.isArray(this.queuedFilesAnnotations[id])
? this.queuedFilesAnnotations[id].length
: 0;
const buttons = `
<div class="tabulator-icon-buttons" data-id=${id}>
<dbp-icon-button icon-name="keyword-research" class="preview-button" aria-label="${i18n.t('preview-file-button-title')}" title="${i18n.t('preview-file-button-title')}" style="font-size: 24px;"></dbp-icon-button>
${annotations ?
`<span class="annotation-wrapper" style="display: inline-grid; grid-template-columns: 27px 23px; grid-template-rows: 23px 27px; width: 50px; height: 50px; position: relative;">
<dbp-icon-button icon-name="bubble" class="annotation-button"
aria-label="${i18n.t('annotation-button-title')}" title="${i18n.t('annotation-button-title')}"
style="font-size: 24px; grid-area: 1 / 1 / 3 / 3; place-self: center;"></dbp-icon-button>
${annotationCount < 1
? '<span style="position: absolute; font-size: 19px; top: 21%; left: 40%; font-weight: bold;pointer-events:none;">+</span>'
: `<span title="${i18n.t('annotations-count-text', {annotationCount: annotationCount})}"
style="grid-column: 2 / 3; grid-row: 1 / 2; justify-self: start; align-self: end; background: var(--dbp-primary); color: var(--dbp-background);
border: 1px solid var(--dbp-background); border-radius: 100%; display: block; width: 21px; height: 21px; text-align: center; line-height: 21px;
font-size: 14px; font-weight: bold; z-index: 3;pointer-events:none;">${annotationCount}</span>`
}
</span>`
: ''
}
<dbp-icon-button icon-name="trash" class="delete-button" aria-label="${i18n.t('remove-queued-file-button-title')}" title="${i18n.t('remove-queued-file-button-title')}" style="font-size: 24px;" data-filename="${fileName}"></dbp-icon-button>
</div>
`;
return buttons;
}
getPositioningSwitch(id, placement, needPositioning) {
const i18n = this._i18n;
const styles = css`
.toggle-wrapper {
--toggle-width: 80px;
--toggle-height: 34px;
--icon-width: 35px;
--icon-height: 28px;
--transition-time: .3s;
--gap: 2px;
--checkmark-color: var(--dbp-muted);
--checkmark-color-need-positioning: var(--dbp-danger);
--dbp-border-radius: 4px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
}
.toggle {
position: relative;
display: inline-block;
padding: 0 8px;
}
.label-text {
line-height: var(--toggle-height);
}
/* the switch */
label.toggle-item {
width: var(--toggle-width);
background: var(--dbp-muted);
color: var(--dbp-background);
height: var(--toggle-height);
display: block;
border-radius: var(--dbp-border-radius);
border: 1px solid var(--dbp-muted);
position: relative;
transition: all var(--transition-time) ease;
cursor: pointer;
margin: 0;
}
label.toggle-item.on {
background-color: var(--dbp-info);
border: 1px solid var(--dbp-info);
}
.label-off,
.label-on {
font-weight: bold;
position: absolute;
transition: opacity .1s ease;
transition-delay: .3s;
opacity: 1;
height: var(--toggle-height);
line-height: var(--toggle-height);
}
.label-off {
/*left: calc(100% - var(--icon-size) - var(--gap));*/
right: 6px;
}
.label-on {
/*right: calc(100% - var(--icon-size) - var(--gap));*/
left: 6px;
}
input {
height: 40px;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 40px;
margin: 0;
padding: 0;
}
.toggle {
/* keyboard focus visibility */
.input-checkbox:focus-visible + .toggle-item {
box-shadow:0px 0px 3px 1px var(--dbp-primary);
}
/* the button */
.check {
border-radius: var(--dbp-border-radius);
width: var(--icon-width);
height: var(--icon-height);
position: absolute;
background: var(--dbp-background);
transition: .4s ease;
top: var(--gap);
bottom: var(--gap);
left: var(--gap);
}
}
.need-positioning {
label.toggle-item {
border-color: var(--checkmark-color-need-positioning);
background-color: var(--checkmark-color-need-positioning);
color: var(--dbp-background);
}
}
.hidden {
display: none;
}
.sr-only {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
height: 1px;
width: 1px;
word-wrap: normal;
}
/* animation */
.input-checkbox:checked + label {
.check {
left: calc(100% - var(--icon-width) - var(--gap));
}
}
`;
const checkbox = `
<style>${unsafeCSS(styles)}</style>
<div class="toggle-wrapper">
<div class="toggle ${needPositioning ? 'need-positioning' : ''}" data-need-positioning="${needPositioning ? 'true' : 'false' }">
<input id="toggle-${id}" class="input-checkbox" type="checkbox" role="switch" ${placement == 'manual' ? 'checked="checked"' : ''}"/>
<label class="toggle-item ${placement == 'manual' ? 'on' : 'off'}" for="toggle-${id}" data-row-id="${id}">
<span class="label-on" ${placement == 'manual' ? '' : 'aria-hidden="true"'}">${i18n.t('toggle-switch-label-text-on')}</span>
<span class="label-off" ${placement == 'manual' ? 'aria-hidden="true"' : ''}">${i18n.t('toggle-switch-label-text-off')}</span>
<div class="check"></div>
</label>
</div>
</div>
`;
return checkbox;
}
getDownloadButtonHtml(id, file) {
const i18n = this._i18n;
const ICON_SIZE = '24px';
let controlDiv = document.createElement('div');
controlDiv.classList.add('tabulator-download-button');
// Download button
const btnDownload = document.createElement('dbp-icon-button');
btnDownload.setAttribute('icon-name', 'download');
btnDownload.classList.add('download-button');
btnDownload.setAttribute('aria-label', i18n.t('download-file-button-title'));
btnDownload.setAttribute('title', i18n.t('download-file-button-title'));
btnDownload.style['font-size'] = ICON_SIZE;
// const btnDownload = `<dbp-icon-button icon-name="download" class="download-button" aria-label="${i18n.t('download-file-button-title')}" title="${i18n.t('download-file-button-title')}" style="font-size: 24px;"></dbp-icon-button>`;
btnDownload.addEventListener("click", async (event) => {
event.stopPropagation();
this.downloadFileClickHandler(file, 'file-download-' + id);
this.tableSignedFilesTable.tabulatorTable.updateData([{
index: id,
fileName: `<span id="file-download-${id}">${file.name}</span>
<dbp-icon name="download-complete"
style="font-size: 24px;margin-bottom:8px;margin-left:24px;"
title="${i18n.t('download-file-completed')}"
aria-label="${i18n.t('download-file-completed')}">`
}]
);
});
controlDiv.appendChild(btnDownload);
return controlDiv;
}
getFailedButtonsHtml(id, data) {
const i18n = this._i18n;
const ICON_SIZE = '24px';
let controlDiv = document.createElement('div');
controlDiv.classList.add('tabulator-failed-buttons');
// Re upload button
const btnReupload = document.createElement('dbp-icon-button');
btnReupload.setAttribute('icon-name', 'reload');
btnReupload.classList.add('re-upload-button');
btnReupload.setAttribute('aria-label', i18n.t('re-upload-file-button-title'));
btnReupload.setAttribute('title', i18n.t('re-upload-file-button-title'));
btnReupload.style['font-size'] = ICON_SIZE;
btnReupload.addEventListener("click", async (event) => {
event.stopPropagation();
this.fileQueueingClickHandler(data.file, id);
});
controlDiv.appendChild(btnReupload);
// Delete button
const btnDelete = document.createElement('dbp-icon-button');
btnDelete.setAttribute('icon-name', 'trash');
btnDelete.classList.add('delete-button');
btnDelete.setAttribute('aria-label', i18n.t('remove-failed-file-button-title'));
btnDelete.setAttribute('title', i18n.t('remove-failed-file-button-title'));
btnDelete.addEventListener("click", async (event) => {
event.stopPropagation();
this.takeFailedFileFromQueue(id);
});
controlDiv.appendChild(btnDelete);
return controlDiv;
}
/**
* Create tabulator table for queued files
*
*/
setQueuedFilesTabulatorTable() {
const i18n = this._i18n;
let langs = {
'en': {
columns: {
'fileName': i18n.t('table-header-file-name', {lng: 'en'}),
'fileSize': i18n.t('table-header-file-size', {lng: 'en'}),
'positioning': i18n.t('table-header-positioning', {lng: 'en'}),
'buttons': i18n.t('table-header-buttons', {lng: 'en'}),
},
},
'de': {
columns: {
'fileName': i18n.t('table-header-file-name', {lng: 'de'}),
'fileSize': i18n.t('table-header-file-size', {lng: 'de'}),
'positioning': i18n.t('table-header-positioning', {lng: 'de'}),
'buttons': i18n.t('table-header-buttons', {lng: 'de'}),
},
},
};
const queuedFilesOptions = {
local: 'en',
langs: langs,
layout: 'fitColumns',
responsiveLayout: 'collapse',
responsiveLayoutCollapseStartOpen: false,
index: 'index',
rowHeight: 54,
columns: [
{
title: '#',
field: 'index',
hozAlign: 'center',
headerHozAlign: 'center',
width: 40,
visible: false
},
{
title: '',
field: 'toggle',
hozAlign: 'center',
width: 65,
formatter: 'responsiveCollapse',
headerHozAlign: 'center',
headerSort:false,
responsive: 0
},
{
title: 'fileName',
field: 'fileName',
sorter: 'string',
minWidth: 250,
widthGrow: 3,
hozAlign: 'left',
formatter: 'html',
responsive: 0
},
{
title: 'fileSize',
field: 'fileSize',
sorter: 'string',
minWidth: 120,
hozAlign: 'right',
headerHozAlign: 'right',
formatter: 'plaintext',
responsive: 3
},
// {
// title: 'profile',
// field: 'profile',
// sorter: false,
// width: 120,
// hozAlign: 'center',
// headerHozAlign: 'center',
// formatter: 'html',
// // visible: false
// responsive: 2
// },
{
title: 'positioning',
field: 'positioning',
minWidth: 100,
hozAlign: 'center',
headerHozAlign: 'center',
headerSort: false,
formatter: 'html',
cellClick: (e, cell) => {
this.handlePositionButtonClickEvent(e, cell);
},
responsive: 2,
},
{
title: 'buttons',
field: 'buttons',
sorter: false,
headerSort: false,
width: 160,
hozAlign: 'right',
headerHozAlign: 'center',
formatter: (cell, formatterParams, onRendered) => {
const buttonElements = cell.getValue();
onRendered(() => {
// Add EventListeners
const cellElement = cell.getElement();
const id = cell.getRow().getIndex();
// Preview button eventListener
const previewButton = cellElement.querySelector('.preview-button');
if (previewButton && !previewButton.hasAttribute('data-listener-added')) {
previewButton.addEventListener("click", (event) => {
event.stopPropagation();
this._('#pdf-preview').open();
this._('#pdf-preview dbp-pdf-preview').setAttribute('don-t-show-buttons', '');
this.showPreview(id, false, true);
});
previewButton.setAttribute('data-listener-added', 'true');
}
// Annotation button eventListener
const annotationWrapper = cellElement.querySelector('.annotation-wrapper');
if (annotationWrapper && !annotationWrapper.hasAttribute('data-listener-added')) {
annotationWrapper.addEventListener("click", (event) => {
event.stopPropagation();
this._('#annotation-view').open();
this.showAnnotationView(id, 'text-selected');
});
annotationWrapper.setAttribute('data-listener-added', 'true');
}
// Delete button eventListener
const deleteButton = cellElement.querySelector('.delete-button');
if (deleteButton && !deleteButton.hasAttribute('data-listener-added')) {
deleteButton.addEventListener("click", (event) => {
event.stopPropagation();
const editButton = /** @type {HTMLElement} */ (event.target);
const fileName = editButton.getAttribute('data-filename') || i18n.t('this-file');
const result = confirm(i18n.t('confirm-delete-file', { file: fileName}));
if (result) {
this.takeFileFromQueue(id);
}
});
deleteButton.setAttribute('data-listener-added', 'true');
}
return cellElement;
});
return buttonElements;
},
responsive: 1
},
],
columnDefaults: {
vertAlign: 'middle',
resizable: false,
},
};
let tableFiles =