| @ -0,0 +1,42 @@ | |||
| # Generated by Django 2.2.15 on 2020-09-21 19:55 | |||
| from django.db import migrations, models | |||
| import django.db.models.deletion | |||
| class Migration(migrations.Migration): | |||
| dependencies = [ | |||
| ('core', '0020_auto_20200903_2328'), | |||
| ('poc', '0004_auto_20200917_1008'), | |||
| ] | |||
| operations = [ | |||
| migrations.RenameField( | |||
| model_name='document', | |||
| old_name='docType', | |||
| new_name='doc_type', | |||
| ), | |||
| migrations.RenameField( | |||
| model_name='document', | |||
| old_name='partyId', | |||
| new_name='party_id', | |||
| ), | |||
| migrations.AddField( | |||
| model_name='document', | |||
| name='bceid_user', | |||
| field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='core.BceidUser'), | |||
| ), | |||
| migrations.AddField( | |||
| model_name='document', | |||
| name='length', | |||
| field=models.IntegerField(default=0), | |||
| ), | |||
| migrations.AlterUniqueTogether( | |||
| name='document', | |||
| unique_together={('bceid_user', 'doc_type', 'party_id', 'filename', 'length')}, | |||
| ), | |||
| migrations.RunSQL( | |||
| sql='delete from poc_document' | |||
| ) | |||
| ] | |||
| @ -0,0 +1,20 @@ | |||
| # Generated by Django 2.2.15 on 2020-09-21 20:01 | |||
| from django.db import migrations, models | |||
| import django.db.models.deletion | |||
| class Migration(migrations.Migration): | |||
| dependencies = [ | |||
| ('poc', '0005_auto_20200921_1255'), | |||
| ] | |||
| operations = [ | |||
| migrations.AlterField( | |||
| model_name='document', | |||
| name='bceid_user', | |||
| field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='core.BceidUser'), | |||
| preserve_default=False, | |||
| ), | |||
| ] | |||
| @ -0,0 +1,23 @@ | |||
| # Generated by Django 2.2.15 on 2020-09-21 22:23 | |||
| from django.db import migrations, models | |||
| class Migration(migrations.Migration): | |||
| dependencies = [ | |||
| ('poc', '0006_auto_20200921_1301'), | |||
| ] | |||
| operations = [ | |||
| migrations.AddField( | |||
| model_name='document', | |||
| name='order', | |||
| field=models.IntegerField(default=1), | |||
| ), | |||
| migrations.AddField( | |||
| model_name='document', | |||
| name='rotation', | |||
| field=models.IntegerField(default=0), | |||
| ), | |||
| ] | |||
| @ -0,0 +1,39 @@ | |||
| # Generated by Django 2.2.15 on 2020-09-22 17:27 | |||
| import datetime | |||
| from django.db import migrations, models | |||
| class Migration(migrations.Migration): | |||
| dependencies = [ | |||
| ('core', '0020_auto_20200903_2328'), | |||
| ('poc', '0007_auto_20200921_1523'), | |||
| ] | |||
| operations = [ | |||
| migrations.RenameField( | |||
| model_name='document', | |||
| old_name='party_id', | |||
| new_name='party_code', | |||
| ), | |||
| migrations.RenameField( | |||
| model_name='document', | |||
| old_name='length', | |||
| new_name='size', | |||
| ), | |||
| migrations.RenameField( | |||
| model_name='document', | |||
| old_name='order', | |||
| new_name='sort_order', | |||
| ), | |||
| migrations.AddField( | |||
| model_name='document', | |||
| name='date_uploaded', | |||
| field=models.DateTimeField(default=datetime.datetime.now), | |||
| ), | |||
| migrations.AlterUniqueTogether( | |||
| name='document', | |||
| unique_together={('bceid_user', 'doc_type', 'party_code', 'filename', 'size')}, | |||
| ), | |||
| ] | |||
| @ -0,0 +1,18 @@ | |||
| # Generated by Django 2.2.15 on 2020-09-22 17:33 | |||
| from django.db import migrations, models | |||
| class Migration(migrations.Migration): | |||
| dependencies = [ | |||
| ('poc', '0008_auto_20200922_1027'), | |||
| ] | |||
| operations = [ | |||
| migrations.AlterField( | |||
| model_name='document', | |||
| name='date_uploaded', | |||
| field=models.DateTimeField(auto_now_add=True), | |||
| ), | |||
| ] | |||
| @ -1,249 +0,0 @@ | |||
| <template> | |||
| <div class="item-tile" v-if="file.progress === '100.00' || file.error"> | |||
| <div class="image-wrap" @click.prevent="showImage($event)"> | |||
| <img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="imageStyle"/> | |||
| <i class="fa fa-file-pdf-o" v-if="file.type === 'application/pdf'"></i> | |||
| <button type="button" class="btn-remove" @click.prevent="$emit('remove')" aria-label="Delete"> | |||
| <i class="fa fa-times-circle"></i> | |||
| </button> | |||
| </div> | |||
| <div class="bottom-wrapper"> | |||
| <div class="item-text"> | |||
| {{file.name}} ({{ Math.round(file.size/1024 * 100) / 100 }}KB) | |||
| </div> | |||
| <div class="button-wrapper"> | |||
| <div v-if="!file.active && file.success"> | |||
| <button type="button" @click.prevent="$emit('moveup')" :disabled="index === 0" aria-label="Move down one position"> | |||
| <i class="fa fa-chevron-circle-left"></i> | |||
| </button> | |||
| <button type="button" @click.prevent="$emit('movedown')" :disabled="index >= (fileCount - 1)" aria-label="Move up one position"> | |||
| <i class="fa fa-chevron-circle-right"></i> | |||
| </button> | |||
| <button type="button" aria-label="Rotate counter-clockwise" @click.prevent="$emit('rotateleft')"> | |||
| <i class="fa fa-undo"></i> | |||
| </button> | |||
| <button type="button" aria-label="Rotate clockwise" @click.prevent="$emit('rotateright')"> | |||
| <i class="fa fa-undo fa-flip-horizontal"></i> | |||
| </button> | |||
| </div> | |||
| <div class="alert alert-danger" style="padding: 4px; margin-bottom: 0" v-if="file.error">Upload Error</div> | |||
| </div> | |||
| </div> | |||
| <modal v-model="showModal" ref="modal" :footer="false"> | |||
| <img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="imageStyle"> | |||
| </modal> | |||
| </div> | |||
| <div v-else> | |||
| <ProgressBar :file="file"/> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import ProgressBar from './ProgressBar' | |||
| import { Modal } from 'uiv'; | |||
| export default { | |||
| props: { | |||
| file: Object, | |||
| index: Number, | |||
| fileCount: Number | |||
| }, | |||
| data: function () { | |||
| return { | |||
| showModal: false, | |||
| } | |||
| }, | |||
| components: { | |||
| ProgressBar, | |||
| Modal | |||
| }, | |||
| methods: { | |||
| showImage($event) { | |||
| if ($event.target.tagName !== 'I' && $event.target.tagName !== 'BUTTON') { | |||
| this.showModal = true; | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| rotateVal() { | |||
| let rotation = this.file.rotation; | |||
| while (rotation < 0) { | |||
| rotation += 360; | |||
| } | |||
| while (rotation > 360) { | |||
| rotation -= 360; | |||
| } | |||
| if (rotation === 90) { | |||
| return 90; | |||
| } | |||
| if (rotation === 180) { | |||
| return 180; | |||
| } | |||
| if (rotation === 270) { | |||
| return 270; | |||
| } | |||
| return 0; | |||
| }, | |||
| imageStyle() { | |||
| if (this.rotateVal === 90) { | |||
| let scale = this.file.width / this.file.height; | |||
| let yshift = -100 * scale; | |||
| return "transform:rotate(90deg) translateY("+yshift+"%) scale("+scale+"); transform-origin: top left;"; | |||
| } | |||
| if (this.rotateVal === 270) { | |||
| let scale = this.file.width / this.file.height; | |||
| let xshift = -100 * scale; | |||
| return "transform:rotate(270deg) translateX("+xshift+"%) scale("+scale+"); transform-origin: top left;"; | |||
| } | |||
| if (this.rotateVal === 180) { | |||
| return "transform:rotate(180deg);"; | |||
| } | |||
| return ''; | |||
| } | |||
| }, | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .item-tile { | |||
| margin-bottom: 5px; | |||
| position: relative; | |||
| .image-wrap { | |||
| height: 160px; | |||
| border: 1px solid black; | |||
| border-top-left-radius: 6px; | |||
| border-top-right-radius: 6px; | |||
| background-color: white; | |||
| overflow: hidden; | |||
| position: relative; | |||
| z-index: 2; | |||
| i.fa-file-pdf-o { | |||
| color: silver; | |||
| display: block; | |||
| font-size: 50px; | |||
| margin-left: 15px; | |||
| margin-top: 15px; | |||
| opacity: 0.75; | |||
| } | |||
| &::after { | |||
| font-family: FontAwesome; | |||
| content: "\f06e"; | |||
| position: absolute; | |||
| left: 58px; | |||
| top: 52px; | |||
| font-size: 43px; | |||
| color: transparent; | |||
| } | |||
| &:hover { | |||
| background-color: #6484d3; | |||
| cursor: pointer; | |||
| button.btn-remove { | |||
| background-color: transparent; | |||
| i.fa { | |||
| color: white; | |||
| } | |||
| } | |||
| } | |||
| &:hover::after { | |||
| color: white; | |||
| } | |||
| &:hover img { | |||
| opacity: 0.3; | |||
| } | |||
| } | |||
| .item-text { | |||
| text-align: center; | |||
| min-height: 75px; | |||
| max-height: 75px; | |||
| overflow: hidden; | |||
| padding: 5px; | |||
| line-height: 1.05; | |||
| font-size: 0.95em; | |||
| } | |||
| .button-wrapper { | |||
| text-align: center; | |||
| } | |||
| .bottom-wrapper { | |||
| border-bottom-left-radius: 6px; | |||
| border-bottom-right-radius: 6px; | |||
| border: 1px solid silver; | |||
| background-color: #F2F2F2; | |||
| margin-bottom: 10px; | |||
| } | |||
| button { | |||
| position: relative; | |||
| z-index: 2; | |||
| background-color: transparent; | |||
| border: none; | |||
| outline: none; | |||
| font-size: 22px; | |||
| padding: 0; | |||
| margin-right: 16px; | |||
| &:disabled { | |||
| i.fa { | |||
| opacity: 0.15; | |||
| } | |||
| } | |||
| &:hover { | |||
| cursor: pointer !important; | |||
| } | |||
| i.fa { | |||
| color: #003366; | |||
| } | |||
| &:last-of-type { | |||
| margin-right: 0; | |||
| } | |||
| &:nth-of-type(2) { | |||
| margin-right: 32px; | |||
| } | |||
| &.btn-remove { | |||
| position: absolute; | |||
| top: 130px; | |||
| left: 130px; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| height: 22px; | |||
| line-height: 1; | |||
| z-index: 4; | |||
| i.fa { | |||
| color: #365EBE; | |||
| font-size: 23px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| <style lang="css"> | |||
| .modal-content { | |||
| background-color: inherit; | |||
| box-shadow: none; | |||
| -webket-box-shadow: none; | |||
| border: none; | |||
| } | |||
| .modal-content button.close { | |||
| font-size: 80px; | |||
| margin-right: 75px; | |||
| margin-bottom: -100px; | |||
| } | |||
| </style> | |||
| @ -1,256 +0,0 @@ | |||
| <template> | |||
| <div> | |||
| <h5 class="uploader-label"> | |||
| {{ formInfo.preText }} | |||
| <a href="javascript:void(0)" :id="'Tooltip-' + uniqueId"> | |||
| {{ formInfo.name }} <i class="fa fa-question-circle"></i> | |||
| </a> | |||
| <strong v-if="party === 1"> - For You</strong> | |||
| <strong v-if="party === 2"> - For Your Spouse</strong> | |||
| </h5> | |||
| <tooltip :text="formInfo.help" :target="'#Tooltip-' + uniqueId"></tooltip> | |||
| <label :for="inputId" class="sr-only"> | |||
| {{ formInfo.preText }} {{ formInfo.name }} | |||
| <span v-if="party === 1"> - For You</span> | |||
| <span v-if="party === 2"> - For Your Spouse</span> | |||
| </label> | |||
| <div @dragover="draggingOn" @dragenter="draggingOn" @dragleave="draggingOff" @dragend="draggingOff" @drop="draggingOff"> | |||
| <file-upload | |||
| ref="upload" | |||
| v-model="files" | |||
| :multiple="true" | |||
| :drop="true" | |||
| :drop-directory="false" | |||
| :post-action="postAction" | |||
| :input-id="inputId" | |||
| name="file" | |||
| :class="['drop-zone', dragging ? 'dragging' : '']" | |||
| :data="data" | |||
| @input-file="inputFile" | |||
| @input-filter="inputFilter"> | |||
| <div v-if="files.length === 0" class="placeholder"> | |||
| <i class="fa fa-plus-circle"></i><br> | |||
| <em>Drag and Drop the PDF document or JPG pages here,<br>or click here to Browse for files.</em> | |||
| </div> | |||
| <div v-else class="cards"> | |||
| <div v-for="(file, index) in files" v-bind:key="index" class="card"> | |||
| <item-tile | |||
| :file="file" | |||
| :index="index" | |||
| :file-count="files.length" | |||
| @remove="remove(file)" | |||
| @moveup="moveUp(index)" | |||
| @movedown="moveDown(index)" | |||
| @rotateleft="rotateLeft(index)" | |||
| @rotateright="rotateRight(index)"/> | |||
| </div> | |||
| <div class="card upload-button"> | |||
| <div class="upload-button-wrapper"> | |||
| <i class="fa fa-plus-circle"></i> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </file-upload> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import VueUploadComponent from 'vue-upload-component' | |||
| import { Tooltip } from 'uiv'; | |||
| import ItemTile from './ItemTile' | |||
| import Forms from "../utils/forms"; | |||
| export default { | |||
| props: { | |||
| docType: String, | |||
| party: { type: Number, default: 0 } | |||
| }, | |||
| data: function () { | |||
| return { | |||
| files: [], | |||
| dragging: false | |||
| } | |||
| }, | |||
| components: { | |||
| FileUpload: VueUploadComponent, | |||
| ItemTile, | |||
| Tooltip | |||
| }, | |||
| computed: { | |||
| uniqueId() { | |||
| if (this.party === 0) { | |||
| return this.docType; | |||
| } | |||
| return this.docType + this.party; | |||
| }, | |||
| inputId() { | |||
| return "Uploader-" + this.uniqueId; | |||
| }, | |||
| formInfo() { | |||
| return Forms[this.docType]; | |||
| }, | |||
| postAction() { | |||
| return this.$parent.proxyRootPath + "poc/storage" | |||
| }, | |||
| data() { | |||
| return { | |||
| docType: this.docType, | |||
| partyId: this.party | |||
| }; | |||
| } | |||
| }, | |||
| methods: { | |||
| /** | |||
| * Has changed | |||
| * @param Object|undefined newFile Read only | |||
| * @param Object|undefined oldFile Read only | |||
| * @return undefined | |||
| */ | |||
| inputFile(newFile, oldFile) { | |||
| if (newFile && oldFile && !newFile.active && oldFile.active) { | |||
| // Get response data | |||
| console.log('response', newFile.response) | |||
| if (newFile.xhr) { | |||
| // Get the response status code | |||
| console.log('status', newFile.xhr.status) | |||
| } | |||
| } | |||
| this.$refs.upload.active = true; | |||
| if (newFile) { | |||
| console.log('inputFile newFile=' + newFile.name); | |||
| } | |||
| if (oldFile) { | |||
| console.log('inputFile oldFile=' + oldFile.name); | |||
| } | |||
| }, | |||
| /** | |||
| * Pretreatment | |||
| * @param Object|undefined newFile Read and write | |||
| * @param Object|undefined oldFile Read only | |||
| * @param Function prevent Prevent changing | |||
| * @return undefined | |||
| */ | |||
| inputFilter(newFile, oldFile, prevent) { | |||
| if (newFile && !oldFile) { | |||
| // Filter non-image file | |||
| if (!/\.(jpeg|jpg|png|pdf)$/i.test(newFile.name)) { | |||
| return prevent() | |||
| } | |||
| this.files.forEach(function(f) { | |||
| // prevent duplicates (based on filename and length) | |||
| if (f.name === newFile.name && f.length === newFile.length) { | |||
| return prevent(); | |||
| } | |||
| }); | |||
| } | |||
| // Add extra data to to the file object | |||
| if (newFile) { | |||
| newFile.objectURL = '' | |||
| let URL = window.URL || window.webkitURL | |||
| if (URL && URL.createObjectURL) { | |||
| newFile.objectURL = URL.createObjectURL(newFile.file) | |||
| newFile.rotation = 0; | |||
| const img = new Image(); | |||
| img.onload = function() { | |||
| newFile.width = this.width; | |||
| newFile.height = this.height; | |||
| } | |||
| img.src = newFile.objectURL; | |||
| } | |||
| } | |||
| }, | |||
| remove(file) { | |||
| this.$refs.upload.remove(file) | |||
| }, | |||
| moveUp(old_index) { | |||
| if (old_index >= 1 && this.files.length > 1) { | |||
| this.files.splice(old_index - 1, 0, this.files.splice(old_index, 1)[0]); | |||
| } | |||
| }, | |||
| moveDown(old_index) { | |||
| if (old_index <= this.files.length && this.files.length > 1) { | |||
| this.files.splice(old_index + 1, 0, this.files.splice(old_index, 1)[0]); | |||
| } | |||
| }, | |||
| rotateLeft(index) { | |||
| this.files[index].rotation -= 90; | |||
| }, | |||
| rotateRight(index) { | |||
| this.files[index].rotation += 90; | |||
| }, | |||
| draggingOn() { | |||
| this.dragging = true; | |||
| }, | |||
| draggingOff() { | |||
| this.dragging = false; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .drop-zone { | |||
| background-color: white; | |||
| width: 100%; | |||
| display: block; | |||
| text-align: left; | |||
| border: 2px #365EBE dashed; | |||
| border-radius: 6px; | |||
| padding: 18px; | |||
| &.dragging { | |||
| background-color: #F2E3F2; | |||
| } | |||
| .cards { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| justify-content: left; | |||
| } | |||
| .card { | |||
| flex: 0 1 160px; | |||
| margin-bottom: 10px; | |||
| width: 160px; | |||
| margin-right: 18px; | |||
| &.upload-button { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| } | |||
| } | |||
| .fa-plus-circle { | |||
| font-size: 3rem; | |||
| margin-bottom: 8px; | |||
| color: #365EBE; | |||
| &:hover { | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| .placeholder { | |||
| text-align: center; | |||
| } | |||
| } | |||
| h5.uploader-label { | |||
| display: block; | |||
| margin-top: 30px; | |||
| margin-bottom: 10px; | |||
| font-weight: normal; | |||
| font-size: 1em; | |||
| a { | |||
| font-weight: bold; | |||
| text-decoration: underline; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,155 @@ | |||
| <template> | |||
| <div> | |||
| <div :class="['image-wrap', isValidImage ? 'valid' : '']" @click.prevent="showPreview($event)"> | |||
| <img v-if="isValidImage" :src="file.objectURL" :style="imageStyle"/> | |||
| <i class="fa fa-file-pdf-o" v-if="file.type === 'application/pdf'"></i> | |||
| <button type="button" class="btn-remove" @click.prevent="$emit('removeclick')" aria-label="Delete"> | |||
| <i class="fa fa-times-circle"></i> | |||
| </button> | |||
| </div> | |||
| <modal-preview :file="file" | |||
| :imageStyle="imageStyle" | |||
| :rotate-val="rotateVal" | |||
| :show-modal="showModal" | |||
| @close="closePreview()" /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import ModalPreview from './ModalPreview'; | |||
| import rotateFix from '../../utils/rotation'; | |||
| export default { | |||
| props: { | |||
| file: Object | |||
| }, | |||
| components: { | |||
| ModalPreview | |||
| }, | |||
| data: function () { | |||
| return { | |||
| showModal: false, | |||
| } | |||
| }, | |||
| methods: { | |||
| showPreview($event) { | |||
| if (this.isValidImage) { | |||
| if ($event.target.tagName !== 'I' && $event.target.tagName !== 'BUTTON') { | |||
| this.showModal = true; | |||
| } | |||
| } | |||
| }, | |||
| closePreview() { | |||
| this.showModal = false; | |||
| } | |||
| }, | |||
| computed: { | |||
| isValidImage() { | |||
| return this.file.objectURL && !this.file.error && this.file.type !== 'application/pdf'; | |||
| }, | |||
| rotateVal() { | |||
| return rotateFix(this.file.rotation); | |||
| }, | |||
| imageStyle() { | |||
| if (this.rotateVal === 90) { | |||
| let scale = this.file.width / this.file.height; | |||
| let yshift = -100 * scale; | |||
| return "transform:rotate(90deg) translateY("+yshift+"%) scale("+scale+"); transform-origin: top left;"; | |||
| } | |||
| if (this.rotateVal === 270) { | |||
| let scale = this.file.width / this.file.height; | |||
| let xshift = -100 * scale; | |||
| return "transform:rotate(270deg) translateX("+xshift+"%) scale("+scale+"); transform-origin: top left;"; | |||
| } | |||
| if (this.rotateVal === 180) { | |||
| return "transform:rotate(180deg);"; | |||
| } | |||
| return ''; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .image-wrap { | |||
| height: 160px; | |||
| border: 1px solid black; | |||
| border-top-left-radius: 6px; | |||
| border-top-right-radius: 6px; | |||
| background-color: white; | |||
| overflow: hidden; | |||
| position: relative; | |||
| z-index: 2; | |||
| display: flex; | |||
| align-items: center; | |||
| img { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| } | |||
| i.fa-file-pdf-o { | |||
| color: #F40F02; | |||
| display: block; | |||
| font-size: 50px; | |||
| margin-left: 15px; | |||
| margin-top: 15px; | |||
| opacity: 0.75; | |||
| } | |||
| &::after { | |||
| font-family: FontAwesome; | |||
| content: "\f06e"; | |||
| position: absolute; | |||
| left: 58px; | |||
| font-size: 43px; | |||
| color: transparent; | |||
| } | |||
| &.valid:hover { | |||
| background-color: #6484d3; | |||
| cursor: pointer; | |||
| button.btn-remove { | |||
| background-color: transparent; | |||
| i.fa { | |||
| color: white; | |||
| } | |||
| } | |||
| } | |||
| &:hover::after { | |||
| color: white; | |||
| } | |||
| &:hover img { | |||
| opacity: 0.3; | |||
| } | |||
| } | |||
| button { | |||
| &.btn-remove { | |||
| position: absolute; | |||
| top: 130px; | |||
| left: 130px; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| height: 18px; | |||
| line-height: 1; | |||
| z-index: 4; | |||
| i.fa { | |||
| color: #365EBE; | |||
| font-size: 23px; | |||
| &::before { | |||
| display: block; | |||
| margin-top: -2px; | |||
| margin-left: -1px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,141 @@ | |||
| <template> | |||
| <div class="item-tile" v-if="file.progress === '100.00' || file.error"> | |||
| <uploaded-image :file="file" :image-style="imageStyle" @imageclick="showPreview" @removeclick="$emit('remove')" /> | |||
| <div class="bottom-wrapper"> | |||
| <div class="item-text"> | |||
| {{file.name}} <span class="no-wrap">({{ Math.round(file.size/1024/1024 * 100) / 100 }} MB)</span> | |||
| </div> | |||
| <div class="button-wrapper"> | |||
| <div v-if="!file.active && file.success && !isPdf"> | |||
| <button type="button" @click.prevent="$emit('moveup')" :disabled="index === 0" aria-label="Move down one position"> | |||
| <i class="fa fa-chevron-circle-left"></i> | |||
| </button> | |||
| <button type="button" @click.prevent="$emit('movedown')" :disabled="index >= (fileCount - 1)" aria-label="Move up one position"> | |||
| <i class="fa fa-chevron-circle-right"></i> | |||
| </button> | |||
| <button type="button" aria-label="Rotate counter-clockwise" @click.prevent="$emit('rotateleft')"> | |||
| <i class="fa fa-undo"></i> | |||
| </button> | |||
| <button type="button" aria-label="Rotate clockwise" @click.prevent="$emit('rotateright')"> | |||
| <i class="fa fa-undo fa-flip-horizontal"></i> | |||
| </button> | |||
| </div> | |||
| <div class="alert alert-danger" v-if="file.error">Upload Error</div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div v-else> | |||
| <progress-bar :file="file"/> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import UploadedImage from './Image' | |||
| import ProgressBar from './ProgressBar' | |||
| export default { | |||
| props: { | |||
| file: Object, | |||
| index: Number, | |||
| fileCount: Number | |||
| }, | |||
| data: function () { | |||
| return { | |||
| showModal: false, | |||
| } | |||
| }, | |||
| components: { | |||
| ProgressBar, | |||
| UploadedImage | |||
| }, | |||
| methods: { | |||
| showPreview() { | |||
| this.showModal = true; | |||
| }, | |||
| closePreview() { | |||
| this.showModal = false; | |||
| } | |||
| }, | |||
| computed: { | |||
| isPdf() { | |||
| return this.file.type === 'application/pdf'; | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .item-tile { | |||
| margin-bottom: 5px; | |||
| position: relative; | |||
| .item-text { | |||
| text-align: center; | |||
| min-height: 75px; | |||
| max-height: 75px; | |||
| overflow: hidden; | |||
| padding: 5px; | |||
| line-height: 1.05; | |||
| font-size: 0.95em; | |||
| .no-wrap { | |||
| white-space: nowrap; | |||
| } | |||
| } | |||
| .button-wrapper { | |||
| text-align: center; | |||
| } | |||
| .bottom-wrapper { | |||
| border-bottom-left-radius: 6px; | |||
| border-bottom-right-radius: 6px; | |||
| border: 1px solid silver; | |||
| background-color: #F2F2F2; | |||
| margin-bottom: 10px; | |||
| .alert-danger { | |||
| margin-bottom: 0; | |||
| padding: 0; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| <style lang="scss"> | |||
| .item-tile { | |||
| button { | |||
| position: relative; | |||
| z-index: 2; | |||
| background-color: transparent; | |||
| border: none; | |||
| outline: none; | |||
| font-size: 22px; | |||
| padding: 0; | |||
| margin-right: 16px; | |||
| &:disabled { | |||
| i.fa { | |||
| opacity: 0.15; | |||
| } | |||
| } | |||
| &:hover { | |||
| cursor: pointer !important; | |||
| opacity: 0.8; | |||
| } | |||
| i.fa { | |||
| color: #003366; | |||
| } | |||
| &:last-of-type { | |||
| margin-right: 0; | |||
| } | |||
| &:nth-of-type(2) { | |||
| margin-right: 32px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,381 @@ | |||
| <template> | |||
| <div> | |||
| <h5 class="uploader-label"> | |||
| {{ formDef.preText }} | |||
| <a href="javascript:void(0)" :id="'Tooltip-' + uniqueId"> | |||
| {{ formDef.name }} <i class="fa fa-question-circle"></i> | |||
| </a> | |||
| <strong v-if="party === 1"> - For You</strong> | |||
| <strong v-if="party === 2"> - For Your Spouse</strong> | |||
| </h5> | |||
| <tooltip :text="formDef.help" :target="'#Tooltip-' + uniqueId"></tooltip> | |||
| <label :for="inputId" class="sr-only"> | |||
| {{ formDef.preText }} {{ formDef.name }} | |||
| <span v-if="party === 1"> - For You</span> | |||
| <span v-if="party === 2"> - For Your Spouse</span> | |||
| </label> | |||
| <div @dragover="dragOn" @dragenter="dragOn" @dragleave="dragOff" @dragend="dragOff" @drop="dragOff"> | |||
| <file-upload | |||
| ref="upload" | |||
| v-model="files" | |||
| :multiple="true" | |||
| :maximum="maxFiles" | |||
| :size="maxMegabytes * 1024 * 1024" | |||
| :drop="true" | |||
| :drop-directory="false" | |||
| :post-action="postAction" | |||
| :input-id="inputId" | |||
| name="file" | |||
| :class="['drop-zone', dragging ? 'dragging' : '']" | |||
| :data="inputKeys" | |||
| @input-file="inputFile" | |||
| @input-filter="inputFilter"> | |||
| <div v-if="files.length === 0" class="placeholder"> | |||
| <i class="fa fa-plus-circle"></i><br> | |||
| <em>Drag and Drop the PDF document or JPG pages here,<br>or click here to Browse for files.</em> | |||
| </div> | |||
| <template v-else> | |||
| <div class="text-danger error-top" v-if="fileErrors === 1"> | |||
| <strong>One file has failed to upload to the server. Please remove the file marked 'Upload Error' and try uploading it again.</strong> | |||
| </div> | |||
| <div class="text-danger error-top" v-if="fileErrors > 1"> | |||
| <strong>Some files have failed to upload to the server. Please remove the files marked 'Upload Error' and try uploading them again.</strong> | |||
| </div> | |||
| <div class="text-danger error-top" v-if="tooBig"> | |||
| <strong>The total of all uploaded files for this form cannot exceed {{ maxMegabytes }} MB. | |||
| Please reduce the size of your files so the total is below this limit.</strong> | |||
| </div> | |||
| <div class="cards"> | |||
| <div v-for="(file, index) in files" v-bind:key="index" class="card"> | |||
| <item-tile | |||
| :file="file" | |||
| :index="index" | |||
| :file-count="files.length" | |||
| @remove="remove(file)" | |||
| @moveup="moveUp(index)" | |||
| @movedown="moveDown(index)" | |||
| @rotateleft="rotateLeft(index)" | |||
| @rotateright="rotateRight(index)"/> | |||
| </div> | |||
| <div class="card upload-button" v-if="!tooBig"> | |||
| <div class="upload-button-wrapper"> | |||
| <i class="fa fa-plus-circle"></i> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="text-danger pull-right error-bottom" v-if="tooBig"> | |||
| <em> | |||
| <strong> | |||
| (Total {{ Math.round(totalSize/1024/1024 * 100) / 100 }} | |||
| MB of {{ maxMegabytes }} MB) | |||
| </strong> | |||
| </em> | |||
| </div> | |||
| </template> | |||
| </file-upload> | |||
| </div> | |||
| <div class="pull-right" v-if="!tooBig"> | |||
| <em>(Maximum {{ maxMegabytes }} MB)</em> | |||
| </div> | |||
| <modal ref="warningModal" v-model="showWarning"> | |||
| {{ warningText }} | |||
| </modal> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import VueUploadComponent from 'vue-upload-component' | |||
| import { Tooltip, Modal } from 'uiv'; | |||
| import ItemTile from './ItemTile' | |||
| import FormDefinitions from "../../utils/forms"; | |||
| import rotateFix from '../../utils/rotation'; | |||
| export default { | |||
| props: { | |||
| docType: String, | |||
| party: { type: Number, default: 0 } | |||
| }, | |||
| data: function () { | |||
| return { | |||
| maxFiles: 30, | |||
| maxMegabytes: 10, | |||
| files: [], | |||
| dragging: false, | |||
| showWarning: false, | |||
| warningText: "", | |||
| isDirty: false | |||
| } | |||
| }, | |||
| components: { | |||
| FileUpload: VueUploadComponent, | |||
| ItemTile, | |||
| Tooltip, | |||
| Modal | |||
| }, | |||
| computed: { | |||
| inputId() { | |||
| return "Uploader-" + this.uniqueId; | |||
| }, | |||
| inputKeys() { | |||
| return { | |||
| doc_type: this.docType, | |||
| party_code: this.party | |||
| }; | |||
| }, | |||
| formDef() { | |||
| return FormDefinitions[this.docType]; | |||
| }, | |||
| postAction() { | |||
| return this.$parent.proxyRootPath + "poc/storage" | |||
| }, | |||
| uniqueId() { | |||
| if (this.party === 0) { | |||
| return this.docType; | |||
| } | |||
| return this.docType + this.party; | |||
| }, | |||
| totalSize() { | |||
| let size = 0; | |||
| this.files.forEach((file) => { | |||
| if (!file.error) { | |||
| size += file.size; | |||
| } | |||
| }); | |||
| return size; | |||
| }, | |||
| fileErrors() { | |||
| let count = 0; | |||
| this.files.forEach((file) => { | |||
| if (file.error) { | |||
| count++; | |||
| } | |||
| }); | |||
| return count; | |||
| }, | |||
| tooBig() { | |||
| return this.totalSize > this.maxMegabytes * 1024 * 1024; | |||
| } | |||
| }, | |||
| methods: { | |||
| inputFile(newFile, oldFile) { | |||
| // upload is complete | |||
| if (newFile && oldFile && !newFile.active && oldFile.active) { | |||
| // todo: send metadata to the server | |||
| console.log('Upload Complete; file=' + newFile.name) | |||
| this.saveMetaData(); | |||
| if (newFile.xhr) { | |||
| // Get the response status code (we can use this for error handling) | |||
| if (newFile.xhr.status !== 200) { | |||
| // todo: handler errors | |||
| this.showError('Error: ' + newFile.xhr.statusText); | |||
| console.log('status', newFile.xhr.status) | |||
| } | |||
| } | |||
| } | |||
| this.$refs.upload.active = true; | |||
| }, | |||
| inputFilter(newFile, oldFile, prevent) { | |||
| if (newFile && !oldFile) { | |||
| // Filter non-image file | |||
| if (!/\.(jpeg|jpg|gif|png|pdf)$/i.test(newFile.name)) { | |||
| this.showError('Unsupported file type. Allowed extensions are jpeg, jpg, gif,png and pdf.'); | |||
| return prevent() | |||
| } | |||
| this.files.forEach((file) => { | |||
| // prevent duplicates (based on filename and length) | |||
| if (file.name === newFile.name && file.length === newFile.length) { | |||
| this.showError('Duplicate file: ' + newFile.name); | |||
| return prevent(); | |||
| } | |||
| }); | |||
| } | |||
| if (newFile) { | |||
| // make sure the user isn't uploading more MB of files than allowed | |||
| if (this.totalSize > this.maxMegabytes * 1024 * 1024) { | |||
| this.showError('The total of all uploaded files for this form cannot exceed ' + this.maxMegabytes + ' MB. Please reduce the size of your files so the total is below this limit.'); | |||
| // only allow one file over the limit (so we can show the red messaging on the screen) | |||
| let previousTotalSize = 0; | |||
| this.files.forEach((file) => { | |||
| if ((file.name !== newFile.name || file.size !== newFile.size) && !file.error) { | |||
| previousTotalSize += file.size; | |||
| } | |||
| }); | |||
| // if the user is more than one file over the limit, then block the upload | |||
| if (previousTotalSize > this.maxMegabytes * 1024 * 1024) { | |||
| this.$refs.upload.remove(newFile); | |||
| return prevent(); | |||
| } | |||
| } | |||
| // if it's a PDF, make sure it's the only item being uploaded | |||
| if (newFile.type === 'application/pdf') { | |||
| if (this.files.length > 0) { | |||
| if (this.files[0].name != newFile.name || this.files[0].length != newFile.length) { | |||
| this.showError('Only one PDF is allowed per form, and PDF documents cannot be combined with images.'); | |||
| this.$refs.upload.remove(newFile); | |||
| return prevent(); | |||
| } | |||
| } | |||
| } else { | |||
| // if it's not a PDF, make sure there are no PDFs already uplaoded | |||
| this.files.forEach((file) => { | |||
| if (file.type === 'application/pdf') { | |||
| this.showError('PDF documents cannot be combined with images.'); | |||
| this.$refs.upload.remove(newFile); | |||
| return prevent(); | |||
| } | |||
| }); | |||
| } | |||
| // Add extra data to to the file object | |||
| newFile.objectURL = '' | |||
| let URL = window.URL || window.webkitURL | |||
| if (URL && URL.createObjectURL) { | |||
| newFile.objectURL = URL.createObjectURL(newFile.file) | |||
| newFile.rotation = 0; | |||
| const img = new Image(); | |||
| img.onload = function() { | |||
| newFile.width = this.width; | |||
| newFile.height = this.height; | |||
| } | |||
| img.src = newFile.objectURL; | |||
| } | |||
| } | |||
| }, | |||
| remove(file) { | |||
| // todo: call the API to remove the file | |||
| this.$refs.upload.remove(file) | |||
| }, | |||
| moveUp(old_index) { | |||
| if (old_index >= 1 && this.files.length > 1) { | |||
| this.files.splice(old_index - 1, 0, this.files.splice(old_index, 1)[0]); | |||
| } | |||
| this.isDirty = true; | |||
| }, | |||
| moveDown(old_index) { | |||
| if (old_index <= this.files.length && this.files.length > 1) { | |||
| this.files.splice(old_index + 1, 0, this.files.splice(old_index, 1)[0]); | |||
| } | |||
| this.isDirty = true; | |||
| }, | |||
| rotateLeft(index) { | |||
| this.files[index].rotation -= 90; | |||
| this.isDirty = true; | |||
| }, | |||
| rotateRight(index) { | |||
| this.files[index].rotation += 90; | |||
| this.isDirty = true; | |||
| }, | |||
| dragOn() { | |||
| this.dragging = true; | |||
| }, | |||
| dragOff() { | |||
| this.dragging = false; | |||
| }, | |||
| showError(message) { | |||
| this.warningText = message; | |||
| this.showWarning = true; | |||
| }, | |||
| saveMetaData() { | |||
| let allFiles = []; | |||
| this.files.forEach((file) => { | |||
| allFiles.push({ | |||
| filename: file.name, | |||
| size: file.size, | |||
| rotation: rotateFix(file.rotation) | |||
| }); | |||
| }); | |||
| const data = { | |||
| docType: this.docType, | |||
| partyCode: this.party, | |||
| files: allFiles | |||
| }; | |||
| console.log('Call API', data); | |||
| } | |||
| }, | |||
| created() { | |||
| // call the API to update the metadata every second, but only if the data has changed | |||
| // (throttling requests because rotating and re-ordering images can cause a lot of traffic) | |||
| setInterval(() => { | |||
| if (this.isDirty) { | |||
| this.saveMetaData(); | |||
| this.isDirty = false; | |||
| } | |||
| }, 1000); | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .drop-zone { | |||
| background-color: white; | |||
| width: 100%; | |||
| display: block; | |||
| text-align: left; | |||
| border: 2px #365EBE dashed; | |||
| border-radius: 6px; | |||
| padding: 18px; | |||
| &.dragging { | |||
| background-color: #F2E3F2; | |||
| } | |||
| .cards { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| justify-content: left; | |||
| } | |||
| .card { | |||
| flex: 0 1 160px; | |||
| margin-bottom: 10px; | |||
| width: 160px; | |||
| margin-right: 18px; | |||
| &.upload-button { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: center; | |||
| } | |||
| } | |||
| .fa-plus-circle { | |||
| font-size: 3rem; | |||
| margin-bottom: 8px; | |||
| color: #365EBE; | |||
| } | |||
| .placeholder { | |||
| text-align: center; | |||
| } | |||
| .error-top { | |||
| padding-bottom: 16px; | |||
| } | |||
| .error-bottom { | |||
| margin-bottom: -10px; | |||
| } | |||
| } | |||
| h5.uploader-label { | |||
| display: block; | |||
| margin-top: 30px; | |||
| margin-bottom: 10px; | |||
| font-weight: normal; | |||
| font-size: 1em; | |||
| a { | |||
| font-weight: bold; | |||
| text-decoration: underline; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,63 @@ | |||
| <template> | |||
| <modal v-model="showModal" class="image-preview-modal" ref="modal" :footer="false" @hide="$emit('close')"> | |||
| <img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="modalImageStyle" :data-rotate="rotateVal"> | |||
| </modal> | |||
| </template> | |||
| <script> | |||
| import { Modal } from 'uiv'; | |||
| export default { | |||
| props: { | |||
| file: Object, | |||
| imageStyle: String, | |||
| rotateVal: Number, | |||
| showModal: Boolean | |||
| }, | |||
| components: { | |||
| Modal | |||
| }, | |||
| computed: { | |||
| modalImageStyle() { | |||
| let extraCss = ''; | |||
| if (this.rotateVal === 90 || this.rotateVal === 270) { | |||
| extraCss = ' width: 100%; height: inherit !important;'; | |||
| } | |||
| return this.imageStyle + extraCss; | |||
| } | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| .image-preview-modal { | |||
| .modal-dialog { | |||
| max-width: 780px; | |||
| width: inherit; | |||
| text-align: center; | |||
| } | |||
| .modal-content { | |||
| background-color: transparent; | |||
| box-shadow: none; | |||
| -webket-box-shadow: none; | |||
| border: none; | |||
| .modal-body, .modal-header { | |||
| padding: 0 !important; | |||
| } | |||
| .modal-body { | |||
| img { | |||
| max-width: 100%; | |||
| } | |||
| } | |||
| button.close { | |||
| font-size: 40px; | |||
| color: white; | |||
| opacity: 1; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,16 @@ | |||
| export default function(rotation) { | |||
| while (rotation < 0) { | |||
| rotation += 360; | |||
| } | |||
| while (rotation > 360) { | |||
| rotation -= 360; | |||
| } | |||
| switch (rotation) { | |||
| case 90: | |||
| case 180: | |||
| case 270: | |||
| return rotation; | |||
| default: | |||
| return 0; | |||
| } | |||
| }; | |||