| @ -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; | |||||
| } | |||||
| }; | |||||