Skip to content

Commit

Permalink
#920: add reader support for file input (#1520)
Browse files Browse the repository at this point in the history
* #920: add reader support for file input

* #920: fix NaN on file size

* #920: merge with dev

* #920:fix nvda bugs
  • Loading branch information
StephanStrehlerCGI authored Apr 9, 2024
1 parent eb03b78 commit f9b3c4a
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 108 deletions.
4 changes: 1 addition & 3 deletions digiwf-apps/packages/apps/digiwf-tasklist/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ declare module 'vue' {
KeyboardAccessibilityIcon: typeof import('./src/components/UI/icons/KeyboardAccessibilityIcon.vue')['default']
LeaveSiteDialog: typeof import('./src/components/common/LeaveSiteDialog.vue')['default']
LoadingFab: typeof import('./src/components/UI/LoadingFab.vue')['default']
Notifcation: typeof import('./src/components/common/Notifcation.vue')['default']
PdfOutput: typeof import('./src/components/form/PdfOutput.vue')['default']
ProcessDefinitionItem: typeof import('./src/components/process/ProcessDefinitionItem.vue')['default']
ProcessInstanceItem: typeof import('./src/components/process/ProcessInstanceItem.vue')['default']
SearchField: typeof import('./src/components/common/SearchField.vue')['default']
Snackbar: typeof import('./src/components/common/Snackbar.vue')['default']
SortBySelect: typeof import('./src/components/common/SortBySelect.vue')['default']
StatementIcon: typeof import('./src/components/UI/icons/StatementIcon.vue')['default']
TaskFollowUpDialog: typeof import('./src/components/task/TaskFollowUpDialog.vue')['default']
Expand Down Expand Up @@ -87,13 +85,13 @@ declare module 'vue' {
VListItemTitle: typeof import('vuetify/lib')['VListItemTitle']
VMain: typeof import('vuetify/lib')['VMain']
VMenu: typeof import('vuetify/lib')['VMenu']
VMessages: typeof import('vuetify/lib')['VMessages']
VMultiUserInput: typeof import('./src/components/schema/VMultiUserInput.vue')['default']
VNavigationDrawer: typeof import('vuetify/lib')['VNavigationDrawer']
VProgressCircular: typeof import('vuetify/lib')['VProgressCircular']
VRow: typeof import('vuetify/lib')['VRow']
VSelect: typeof import('vuetify/lib')['VSelect']
VSimpleTable: typeof import('vuetify/lib')['VSimpleTable']
VSnackbar: typeof import('vuetify/lib')['VSnackbar']
VSpacer: typeof import('vuetify/lib')['VSpacer']
VSpeedDial: typeof import('vuetify/lib')['VSpeedDial']
VStepper: typeof import('vuetify/lib')['VStepper']
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,60 @@
<template>
<div>
<a target="_blank" @click="openInTab()">
<v-card class="doc-card mb-2" elevation="2" outlined max-width="350px">
<v-card-title class="text-subtitle-1 title">
<div class="d-flex align-start flex-row">
<v-icon left size="30" class="mr-2">
{{ icon }}
</v-icon>
{{ document.name }}
<v-card class="doc-card mb-2" elevation="2" tabindex="-1" outlined max-width="350px" @click="openInTab()">
<v-card-title class="text-subtitle-1 title">
<div class="d-flex align-start flex-row" tabindex="0">
<v-icon left size="30" class="mr-2" :aria-label="document.name">
{{ icon }}
</v-icon>
{{ document.name }}
</div>
</v-card-title>
<v-card-text>
<div class="preview">
<v-img
v-if="isImage"
class="preview-component"
:src="document.data"
max-width="200px"
:alt="'Bildvorschau von ' + document.name"
>
</v-img>

<vue2-pdf-embed
v-else-if="isPdf"
:source="document.data"
class="preview-component"
:aria-label="'PDF Vorschau von ' + document.name"

/>

<div v-else class="preview-text">Keine Vorschau verfügbar</div>
<div>
<div class="footer" tabindex="0">{{ documentSize }}</div>
<template v-if="!readonly">
<v-btn
class="remove-button ma-1"
elevation="1"
icon
@click.stop="removeDocument"
:aria-label="document.name + ' entfernen'"
>
<v-icon> mdi-delete</v-icon>
</v-btn>
</template>
</div>
</v-card-title>
<v-card-text>
<div class="preview">
<v-img
v-if="isImage"
class="preview-component"
:src="document.data"
max-width="200px"
alt="Image preview..."
>
</v-img>

<vue2-pdf-embed
v-else-if="document.type === 'application/pdf'"
:source="document.data"
class="preview-component"
/>

<div v-else class="preview-text">Keine Vorschau verfügbar</div>
<div>
<div class="footer">{{ formatBytes(0) }}</div>
<template v-if="!readonly">
<v-btn
class="remove-button ma-1"
elevation="1"
icon
@click.stop="removeDocument"
>
<v-icon> mdi-delete</v-icon>
</v-btn>
</template>
</div>
</div>
</v-card-text>
</v-card>
</a>
</div>
</v-card-text>
</v-card>
</div>
</template>

<script lang="ts">
import {computed, defineComponent} from "vue";
import {fileIcons} from "../util";
import {formatBytes} from "@/middleware/fileSize";
import {createBlobUrl} from "@/middleware/url";
export default defineComponent({
props: ['document', 'readonly'],
Expand All @@ -63,47 +66,11 @@ export default defineComponent({
const icon = computed(() => fileIcons[props.document.type] ?? "mdi-file");
const isImage = computed(() => props.document.type.toLowerCase() === "image/jpeg" || props.document.type.toLowerCase() === "image/png");
const blobUrl = (): string => {
const byteCharacters = calcByteCharacters.value;
const byteArrays: Uint8Array[] = [];
for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: props.document.type});
return URL.createObjectURL(blob);
}
const isPdf = computed(() => props.document.type === 'application/pdf')
const openInTab = () => {
const calcVlobUrl = blobUrl();
const link = document.createElement("a");
link.href = calcVlobUrl;
link.setAttribute("download", props.document.name!);
document.body.appendChild(link);
link.click();
}
const formatBytes = (decimals = 2) => {
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(props.document.size) / Math.log(k));
return (
parseFloat((props.document.size / Math.pow(k, i)).toFixed(dm)) +
" " +
sizes[i]
);
const url = createBlobUrl(calcByteCharacters.value, props.document.type);
window.open(url, "_blank")
}
const removeDocument = () => {
Expand All @@ -114,8 +81,9 @@ export default defineComponent({
calcByteCharacters,
icon,
isImage,
isPdf,
openInTab,
formatBytes,
documentSize: formatBytes(props.document.size),
removeDocument
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
type="file"
v-bind="schema['x-props']"
@change="changeInput"
aria-label="Datei hochladen"
>
<template #label>
<span>{{ label }}</span>
<span v-if="isRequired()" aria-hidden="true" style="font-weight: bold; color: red"> *</span>
<span tabindex="0">{{ label }}</span>
<span v-if="isRequired()" aria-hidden="true" style="font-weight: bold; color: red" aria-label="Eingabe ist ein Pflichtfeld"> *</span>
</template>
<template #append-outer>
<v-tooltip v-if="schema.description" :open-on-hover="false" left>
<template v-slot:activator="{ on }">
<v-btn icon retain-focus-on-click @blur="on.blur" @click="on.click">
<v-btn icon retain-focus-on-click @blur="on.blur" @click="on.click" aria-label="Beschreibung anzeigen">
<v-icon> mdi-information</v-icon>
</v-btn>
</template>
Expand Down Expand Up @@ -62,8 +63,17 @@ import {
} from "@/middleware/presignedUrls";
import {checkRequired} from "@/validation/required";
import {getMimeType, validateFileType} from "@/validation/fileType";
import DwfFilePreview from "@/components/DwfFilePreview.vue";
/**
* existing bug!. Prepend icon cannot be overridden for set tabindex="-1". More information https://github.com/vuetifyjs/vuetify/issues/9580
*/
export default defineComponent({
computed: {
DwfFilePreview() {
return DwfFilePreview
}
},
props: [
'valid',
'readonly',
Expand All @@ -79,20 +89,19 @@ export default defineComponent({
'on'
],
setup(props) {
let model = "";
let fileValue = ref<File[] | null>(null);
let data: any = {};
let documents = ref<DocumentData[]>([]);
let errorMessage = ref<string>("");
let isLoading = ref<boolean>(false);
let uuid = "";
const fileValue = ref<File[]>([]);
const data: any = {};
const documents = ref<DocumentData[]>([]);
const errorMessage = ref<string>("");
const isLoading = ref<boolean>(false);
const uuid = ref("");
const maxFiles = props.schema.maxFiles || 10;
const maxFileSize = props.schema.maxFileSize || 10;
const maxTotalSize = props.schema.maxTotalSize;
const mbInByte = 1048576;
const hint = !!maxTotalSize ?
"Es dürfen maximal " + maxFiles + " Dateien mit einer Gesamtgröße von " + maxTotalSize + " MB hochgeladen werden" :
"Es dürfen maximal " + maxFiles + " Dateien hochgeladen werden";
`Es dürfen maximal ${maxFiles} Dateien mit einer Gesamtgröße von ${maxTotalSize} MB hochgeladen werden` :
`Es dürfen maximal ${maxFiles} Dateien hochgeladen werden`;
let rules = props.rules ? props.rules : true;
const apiEndpoint = inject<string>('apiEndpoint');
Expand Down Expand Up @@ -319,7 +328,7 @@ export default defineComponent({
try {
addDocument(event.target?.result, file);
} catch (e: any) {
errorMessage = e.message;
errorMessage.value = e.message;
}
};
reader.readAsArrayBuffer(file);
Expand Down Expand Up @@ -385,20 +394,19 @@ export default defineComponent({
//initialize uuid if enabled
if (props.schema.uuidEnabled) {
if (props.value && props.value.key) {
uuid = props.value.key;
uuid.value = props.value.key;
} else {
uuid = uuidv4();
uuid.value = uuidv4();
}
}
rules.push(() => documents.value.length <= maxFiles || 'Es dürfen maximal ' + maxFiles + ' Dateien übergeben werden');
rules.push(() => documents.value.length <= maxFiles || `Es dürfen maximal ${maxFiles} Dateien übergeben werden`);
if (!!maxTotalSize) {
rules.push(() => validateTotalSize() <= maxTotalSize || 'Die Gesamtgröße aller Dateien darf ' + maxTotalSize + ' MB nicht überschreiten');
rules.push(() => validateTotalSize() <= maxTotalSize || `Die Gesamtgröße aller Dateien darf ${maxTotalSize} MB nicht überschreiten`);
}
loadInitialValues();
})
return {
model,
fileValue,
data,
documents,
Expand All @@ -412,7 +420,6 @@ export default defineComponent({
removeDocument,
isRequired
}
}
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const formatBytes = (size: number, decimals: number = 2) => {
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

const i = Math.floor(Math.log(size) / Math.log(k));

return (
parseFloat((size / Math.pow(k, i)).toFixed(dm)) +
" " +
sizes[i]
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const createBlobUrl = (byteCharacters: string, type: string): string => {

const byteArrays: Uint8Array[] = [];

for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: type});
return URL.createObjectURL(blob);
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
"requiredObject"
],
"fieldType": "file",
"title": "Files",
"title": "Dateien ohne Einschränkungen",
"x-display": "custom-multi-file-input",
"type": "object",
"filePath": "test",
Expand All @@ -249,7 +249,7 @@
"dense": true
},
"fieldType": "file",
"title": "Files with validation",
"title": "Dateien mit Validierung",
"x-display": "custom-multi-file-input",
"type": "object",
"filePath": "test",
Expand Down

0 comments on commit f9b3c4a

Please sign in to comment.