Skip to content

Commit e597281

Browse files
authored
Merge pull request #202 from ditdot-dev/feature/filetype-question
Added File question type
2 parents ddbe65e + d0e8489 commit e597281

File tree

6 files changed

+217
-5
lines changed

6 files changed

+217
-5
lines changed

src/assets/css/common.css

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,67 @@ header.vff-header svg.f-logo {
269269
-webkit-appearance: none;
270270
}
271271

272-
.vff ::-webkit-file-upload-button {
273-
-webkit-appearance: button;
272+
.vff [type="file"] {
273+
appearance: none;
274+
-moz-appearance: none;
275+
-webkit-appearance: none;
276+
border: 0;
277+
outline: 0;
278+
border-radius: 0;
279+
margin: 0 .2em;
280+
padding: .1em 0 .15em;
281+
font-size: .72em;
282+
line-height: normal;
283+
font-weight: 900;
284+
}
285+
286+
.vff input[type=file]:focus{
287+
outline: 1px dotted #000;
288+
outline-offset: 4px;
289+
}
290+
291+
@media (prefers-color-scheme: dark) {
292+
.vff input[type=file]:focus{
293+
outline-color: #fff;
294+
}
295+
296+
}
297+
298+
.vff input[type=file]::-webkit-file-upload-button {
299+
appearance: none;
300+
-moz-appearance: none;
301+
-webkit-appearance: none;
302+
outline: 0;
303+
border: 0;
274304
font: inherit;
305+
font-size: .86em;
306+
font-weight: 400;
307+
margin-right: .7em;
308+
text-align: center;
309+
max-width: 100%;
310+
min-width: 90px;
311+
min-height: 44px;
312+
display: inline-block;
313+
white-space: pre-wrap;
314+
cursor: pointer;
315+
padding: .6em 1.4em;
316+
background-color: #efefef;
317+
}
318+
319+
.vff input[type=file]::-webkit-file-upload-button:active{
320+
color: #000;
321+
}
322+
323+
.vff input[type=file]::file-selector-button{
324+
min-height: 44px;
325+
display: inline-block;
326+
white-space: pre-wrap;
327+
font: inherit;
328+
font-size: .86em;
329+
font-weight: 400;
330+
margin-right: .6em;
331+
max-width: 100%;
332+
min-width: 90px;
275333
}
276334

277335
/*buttons*/
@@ -388,6 +446,7 @@ header.vff-header svg.f-logo {
388446
.vff .f-full-width input[type=url],
389447
.vff .f-full-width input[type=password],
390448
.vff .f-full-width input[type=date],
449+
.vff .f-full-width input[type=file],
391450
.vff .f-full-width textarea,
392451
.vff .f-full-width span.faux-form{
393452
width: 100%;
@@ -811,6 +870,7 @@ header.vff-header svg.f-logo {
811870
/* prevent Android Chrome flickering */
812871
.vff-animate * {
813872
-webkit-backface-visibility: hidden;
873+
backface-visibility: hidden;
814874
}
815875

816876
.vff .f-fade-in {
@@ -923,9 +983,14 @@ header.vff-header svg.f-logo {
923983
.vff input[type=url],
924984
.vff input[type=password],
925985
.vff input[type=date],
986+
.vff input[type=file],
926987
.vff textarea{
927988
font-size: .78em;
928989
}
990+
991+
.vff input[type=file] {
992+
font-size: .64em;
993+
}
929994

930995
.vff .fh2 span.f-sub,
931996
.vff .fh2 span.f-tagline{
@@ -977,11 +1042,16 @@ header.vff-header svg.f-logo {
9771042
.vff input[type=url],
9781043
.vff input[type=password],
9791044
.vff input[type=date],
1045+
.vff input[type=file],
9801046
.vff textarea {
9811047
line-height: 1.4;
9821048
padding: .16em .2em;
9831049
}
9841050

1051+
.vff input[type=file] {
1052+
font-size: .6em;
1053+
}
1054+
9851055
.vff select {
9861056
font-size: .72em;
9871057
padding-top: .2em;

src/components/FlowFormQuestion.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
</a>
8787
</div>
8888

89-
<div v-if="showInvalid()" class="f-invalid" role="alert" aria-live="assertive">{{ language.invalidPrompt }}</div>
89+
<div v-if="showInvalid()" class="f-invalid" role="alert" aria-live="assertive">{{ errorMessage }}</div>
9090
</div>
9191
</div>
9292
</template>
@@ -112,6 +112,7 @@
112112
import FlowFormTextType from './QuestionTypes/TextType.vue'
113113
import FlowFormUrlType from './QuestionTypes/UrlType.vue'
114114
import FlowFormDateType from './QuestionTypes/DateType.vue'
115+
import FlowFormFileType from './QuestionTypes/FileType.vue'
115116
import { IsMobile } from '../mixins/IsMobile'
116117
117118
@@ -130,6 +131,7 @@
130131
FlowFormPhoneType,
131132
FlowFormSectionBreakType,
132133
FlowFormTextType,
134+
FlowFormFileType,
133135
FlowFormUrlType
134136
},
135137
@@ -340,6 +342,16 @@
340342
}
341343
342344
return false
345+
},
346+
347+
errorMessage() {
348+
const q = this.$refs.questionComponent
349+
350+
if (q && q.errorMessage) {
351+
return q.errorMessage
352+
}
353+
354+
return this.language.invalidPrompt
343355
}
344356
}
345357
}

src/components/QuestionTypes/BaseType.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
allowedChars: null,
3636
alwaysAllowedKeys: ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace'],
3737
focused: false,
38-
canReceiveFocus: false
38+
canReceiveFocus: false,
39+
errorMessage: null
3940
}
4041
},
4142
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<template>
2+
<input
3+
ref="input"
4+
type="file"
5+
v-bind:accept="question.accept"
6+
v-bind:multiple="question.multiple"
7+
v-bind:value="modelValue"
8+
v-bind:required="question.required"
9+
v-on:keyup.enter.prevent="onEnter"
10+
v-on:keyup.tab.prevent="onEnter"
11+
v-on:focus="setFocus"
12+
v-on:blur="unsetFocus"
13+
v-on:change="onChange"
14+
/>
15+
</template>
16+
17+
<script>
18+
/*
19+
Copyright (c) 2020 - present, DITDOT Ltd. - MIT Licence
20+
https://github.com/ditdot-dev/vue-flow-form
21+
https://www.ditdot.hr/en
22+
*/
23+
24+
import TextType from './TextType.vue'
25+
import { QuestionType } from '../../models/QuestionModel'
26+
27+
export default {
28+
extends: TextType,
29+
30+
name: QuestionType.File,
31+
32+
mounted() {
33+
if (this.question.accept) {
34+
this.mimeTypeRegex = new RegExp(this.question.accept.replace('*', '[^\\/,]+'))
35+
}
36+
},
37+
38+
methods: {
39+
setAnswer(answer) {
40+
this.question.setAnswer(this.files)
41+
42+
this.answer = answer
43+
this.question.answered = this.isValid()
44+
45+
this.$emit('update:modelValue', answer)
46+
},
47+
48+
showInvalid() {
49+
return this.errorMessage !== null
50+
},
51+
52+
validate() {
53+
this.errorMessage = null
54+
55+
if (this.question.required && !this.hasValue) {
56+
return false
57+
}
58+
59+
if (this.question.accept) {
60+
if (!Array.from(this.files).every(file => this.mimeTypeRegex.test(file.type))) {
61+
this.errorMessage = this.language.formatString(this.language.errorAllowedFileTypes, {
62+
fileTypes: this.question.accept
63+
})
64+
65+
return false
66+
}
67+
}
68+
69+
if (this.question.multiple) {
70+
const fileCount = this.files.length
71+
72+
if (this.question.min !== null && fileCount < +this.question.min) {
73+
this.errorMessage = this.language.formatString(this.language.errorMinFiles, {
74+
min: this.question.min
75+
})
76+
77+
return false
78+
}
79+
80+
if (this.question.max !== null && fileCount > +this.question.max) {
81+
this.errorMessage = this.language.formatString(this.language.errorMaxFiles, {
82+
max: this.question.max
83+
})
84+
85+
return false
86+
}
87+
}
88+
89+
if (this.question.maxSize !== null) {
90+
const fileSize =
91+
Array.from(this.files).reduce((current, file) => current + file.size, 0)
92+
93+
if (fileSize > +this.question.maxSize) {
94+
this.errorMessage = this.language.formatString(this.language.errorMaxFileSize, {
95+
size: this.language.formatFileSize(this.question.maxSize)
96+
})
97+
98+
return false
99+
}
100+
}
101+
102+
return this.$refs.input.checkValidity()
103+
}
104+
},
105+
106+
computed: {
107+
files() {
108+
return this.$refs.input.files
109+
}
110+
}
111+
}
112+
</script>

src/models/LanguageModel.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export default class LanguageModel {
3333
this.ariaSubmitText = 'Press to submit'
3434
this.ariaMultipleChoice = 'Press :letter to select'
3535
this.ariaTypeAnswer = 'Type your answer here'
36+
this.errorAllowedFileTypes = 'Invalid file type. Allowed file types: :fileTypes.'
37+
this.errorMaxFileSize = 'File(s) too large. Maximum allowed file size: :size.'
38+
this.errorMinFiles = 'Too few files added. Minimum allowed files: :min.'
39+
this.errorMaxFiles = 'Too many files added. Maximum allowed files: :max.'
3640

3741
Object.assign(this, options || {})
3842
}
@@ -41,15 +45,25 @@ export default class LanguageModel {
4145
* Inserts a new CSS class into the language model string to format the :string
4246
* Use it in a component's v-html directive: v-html="language.formatString(language.languageString)"
4347
*/
44-
formatString(string) {
48+
formatString(string, replacements) {
4549
return string.replace(/:(\w+)/g, (match, word) => {
4650
if (this[word]) {
4751
return '<span class="f-string-em">' + this[word] + '</span>'
52+
} else if (replacements && replacements[word]) {
53+
return replacements[word]
4854
}
4955

5056
return match
5157
})
5258
}
59+
60+
formatFileSize(bytes) {
61+
const
62+
units = ['B', 'kB', 'MB', 'GB', 'TB'],
63+
i = bytes > 0 ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0
64+
65+
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + units[i];
66+
}
5367
}
5468

5569

src/models/QuestionModel.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const QuestionType = Object.freeze({
1010
Date: 'FlowFormDateType',
1111
Dropdown: 'FlowFormDropdownType',
1212
Email: 'FlowFormEmailType',
13+
File: 'FlowFormFileType',
1314
LongText: 'FlowFormLongTextType',
1415
MultipleChoice: 'FlowFormMultipleChoiceType',
1516
MultiplePictureChoice: 'FlowFormMultiplePictureChoiceType',
@@ -104,6 +105,8 @@ export default class QuestionModel {
104105
this.max = null
105106
this.maxLength = null
106107
this.nextStepOnAnswer = false
108+
this.accept = null
109+
this.maxSize = null
107110

108111
Object.assign(this, options)
109112

0 commit comments

Comments
 (0)